1/*
2 * Copyright (C) 2007 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.ContentValues;
22import android.content.Context;
23import android.content.Intent;
24import android.content.UriMatcher;
25import android.database.Cursor;
26import android.database.sqlite.SQLiteDatabase;
27import android.database.sqlite.SQLiteException;
28import android.database.sqlite.SQLiteOpenHelper;
29import android.database.sqlite.SQLiteQueryBuilder;
30import android.net.Uri;
31import android.os.Binder;
32import android.os.FileUtils;
33import android.os.ParcelFileDescriptor;
34import android.os.UserHandle;
35import android.provider.BaseColumns;
36import android.provider.Telephony;
37import android.provider.Telephony.CanonicalAddressesColumns;
38import android.provider.Telephony.Mms;
39import android.provider.Telephony.Mms.Addr;
40import android.provider.Telephony.Mms.Part;
41import android.provider.Telephony.Mms.Rate;
42import android.provider.Telephony.MmsSms;
43import android.provider.Telephony.Threads;
44import android.text.TextUtils;
45import android.util.Log;
46
47import com.google.android.mms.pdu.PduHeaders;
48import com.google.android.mms.util.DownloadDrmHelper;
49
50import java.io.File;
51import java.io.FileNotFoundException;
52import java.io.IOException;
53
54/**
55 * The class to provide base facility to access MMS related content,
56 * which is stored in a SQLite database and in the file system.
57 */
58public class MmsProvider extends ContentProvider {
59    static final String TABLE_PDU  = "pdu";
60    static final String TABLE_ADDR = "addr";
61    static final String TABLE_PART = "part";
62    static final String TABLE_RATE = "rate";
63    static final String TABLE_DRM  = "drm";
64    static final String TABLE_WORDS = "words";
65    static final String VIEW_PDU_RESTRICTED = "pdu_restricted";
66
67    // The name of parts directory. The full dir is "app_parts".
68    private static final String PARTS_DIR_NAME = "parts";
69
70    @Override
71    public boolean onCreate() {
72        setAppOps(AppOpsManager.OP_READ_SMS, AppOpsManager.OP_WRITE_SMS);
73        mOpenHelper = MmsSmsDatabaseHelper.getInstance(getContext());
74        return true;
75    }
76
77    /**
78     * Return the proper view of "pdu" table for the current access status.
79     *
80     * @param accessRestricted If the access is restricted
81     * @return the table/view name of the mms data
82     */
83    public static String getPduTable(boolean accessRestricted) {
84        return accessRestricted ? VIEW_PDU_RESTRICTED : TABLE_PDU;
85    }
86
87    @Override
88    public Cursor query(Uri uri, String[] projection,
89            String selection, String[] selectionArgs, String sortOrder) {
90        // First check if a restricted view of the "pdu" table should be used based on the
91        // caller's identity. Only system, phone or the default sms app can have full access
92        // of mms data. For other apps, we present a restricted view which only contains sent
93        // or received messages, without wap pushes.
94        final boolean accessRestricted = ProviderUtil.isAccessRestricted(
95                getContext(), getCallingPackage(), Binder.getCallingUid());
96        final String pduTable = getPduTable(accessRestricted);
97
98        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
99
100        // Generate the body of the query.
101        int match = sURLMatcher.match(uri);
102        if (LOCAL_LOGV) {
103            Log.v(TAG, "Query uri=" + uri + ", match=" + match);
104        }
105
106        switch (match) {
107            case MMS_ALL:
108                constructQueryForBox(qb, Mms.MESSAGE_BOX_ALL, pduTable);
109                break;
110            case MMS_INBOX:
111                constructQueryForBox(qb, Mms.MESSAGE_BOX_INBOX, pduTable);
112                break;
113            case MMS_SENT:
114                constructQueryForBox(qb, Mms.MESSAGE_BOX_SENT, pduTable);
115                break;
116            case MMS_DRAFTS:
117                constructQueryForBox(qb, Mms.MESSAGE_BOX_DRAFTS, pduTable);
118                break;
119            case MMS_OUTBOX:
120                constructQueryForBox(qb, Mms.MESSAGE_BOX_OUTBOX, pduTable);
121                break;
122            case MMS_ALL_ID:
123                qb.setTables(pduTable);
124                qb.appendWhere(Mms._ID + "=" + uri.getPathSegments().get(0));
125                break;
126            case MMS_INBOX_ID:
127            case MMS_SENT_ID:
128            case MMS_DRAFTS_ID:
129            case MMS_OUTBOX_ID:
130                qb.setTables(pduTable);
131                qb.appendWhere(Mms._ID + "=" + uri.getPathSegments().get(1));
132                qb.appendWhere(" AND " + Mms.MESSAGE_BOX + "="
133                        + getMessageBoxByMatch(match));
134                break;
135            case MMS_ALL_PART:
136                qb.setTables(TABLE_PART);
137                break;
138            case MMS_MSG_PART:
139                qb.setTables(TABLE_PART);
140                qb.appendWhere(Part.MSG_ID + "=" + uri.getPathSegments().get(0));
141                break;
142            case MMS_PART_ID:
143                qb.setTables(TABLE_PART);
144                qb.appendWhere(Part._ID + "=" + uri.getPathSegments().get(1));
145                break;
146            case MMS_MSG_ADDR:
147                qb.setTables(TABLE_ADDR);
148                qb.appendWhere(Addr.MSG_ID + "=" + uri.getPathSegments().get(0));
149                break;
150            case MMS_REPORT_STATUS:
151                /*
152                   SELECT DISTINCT address,
153                                   T.delivery_status AS delivery_status,
154                                   T.read_status AS read_status
155                   FROM addr
156                   INNER JOIN (SELECT P1._id AS id1, P2._id AS id2, P3._id AS id3,
157                                      ifnull(P2.st, 0) AS delivery_status,
158                                      ifnull(P3.read_status, 0) AS read_status
159                               FROM pdu P1
160                               INNER JOIN pdu P2
161                               ON P1.m_id = P2.m_id AND P2.m_type = 134
162                               LEFT JOIN pdu P3
163                               ON P1.m_id = P3.m_id AND P3.m_type = 136
164                               UNION
165                               SELECT P1._id AS id1, P2._id AS id2, P3._id AS id3,
166                                      ifnull(P2.st, 0) AS delivery_status,
167                                      ifnull(P3.read_status, 0) AS read_status
168                               FROM pdu P1
169                               INNER JOIN pdu P3
170                               ON P1.m_id = P3.m_id AND P3.m_type = 136
171                               LEFT JOIN pdu P2
172                               ON P1.m_id = P2.m_id AND P2.m_type = 134) T
173                   ON (msg_id = id2 AND type = 151)
174                   OR (msg_id = id3 AND type = 137)
175                   WHERE T.id1 = ?;
176                 */
177                qb.setTables(TABLE_ADDR + " INNER JOIN "
178                        + "(SELECT P1._id AS id1, P2._id AS id2, P3._id AS id3, "
179                        + "ifnull(P2.st, 0) AS delivery_status, "
180                        + "ifnull(P3.read_status, 0) AS read_status "
181                        + "FROM " + pduTable + " P1 INNER JOIN " + pduTable + " P2 "
182                        + "ON P1.m_id=P2.m_id AND P2.m_type=134 "
183                        + "LEFT JOIN " + pduTable + " P3 "
184                        + "ON P1.m_id=P3.m_id AND P3.m_type=136 "
185                        + "UNION "
186                        + "SELECT P1._id AS id1, P2._id AS id2, P3._id AS id3, "
187                        + "ifnull(P2.st, 0) AS delivery_status, "
188                        + "ifnull(P3.read_status, 0) AS read_status "
189                        + "FROM " + pduTable + " P1 INNER JOIN " + pduTable + " P3 "
190                        + "ON P1.m_id=P3.m_id AND P3.m_type=136 "
191                        + "LEFT JOIN " + pduTable + " P2 "
192                        + "ON P1.m_id=P2.m_id AND P2.m_type=134) T "
193                        + "ON (msg_id=id2 AND type=151) OR (msg_id=id3 AND type=137)");
194                qb.appendWhere("T.id1 = " + uri.getLastPathSegment());
195                qb.setDistinct(true);
196                break;
197            case MMS_REPORT_REQUEST:
198                /*
199                   SELECT address, d_rpt, rr
200                   FROM addr join pdu on pdu._id = addr.msg_id
201                   WHERE pdu._id = messageId AND addr.type = 151
202                 */
203                qb.setTables(TABLE_ADDR + " join " +
204                        pduTable + " on " + pduTable + "._id = addr.msg_id");
205                qb.appendWhere(pduTable + "._id = " + uri.getLastPathSegment());
206                qb.appendWhere(" AND " + TABLE_ADDR + ".type = " + PduHeaders.TO);
207                break;
208            case MMS_SENDING_RATE:
209                qb.setTables(TABLE_RATE);
210                break;
211            case MMS_DRM_STORAGE_ID:
212                qb.setTables(TABLE_DRM);
213                qb.appendWhere(BaseColumns._ID + "=" + uri.getLastPathSegment());
214                break;
215            case MMS_THREADS:
216                qb.setTables(pduTable + " group by thread_id");
217                break;
218            default:
219                Log.e(TAG, "query: invalid request: " + uri);
220                return null;
221        }
222
223        String finalSortOrder = null;
224        if (TextUtils.isEmpty(sortOrder)) {
225            if (qb.getTables().equals(pduTable)) {
226                finalSortOrder = Mms.DATE + " DESC";
227            } else if (qb.getTables().equals(TABLE_PART)) {
228                finalSortOrder = Part.SEQ;
229            }
230        } else {
231            finalSortOrder = sortOrder;
232        }
233
234        Cursor ret;
235        try {
236            SQLiteDatabase db = mOpenHelper.getReadableDatabase();
237            ret = qb.query(db, projection, selection,
238                    selectionArgs, null, null, finalSortOrder);
239        } catch (SQLiteException e) {
240            Log.e(TAG, "returning NULL cursor, query: " + uri, e);
241            return null;
242        }
243
244        // TODO: Does this need to be a URI for this provider.
245        ret.setNotificationUri(getContext().getContentResolver(), uri);
246        return ret;
247    }
248
249    private void constructQueryForBox(SQLiteQueryBuilder qb, int msgBox, String pduTable) {
250        qb.setTables(pduTable);
251
252        if (msgBox != Mms.MESSAGE_BOX_ALL) {
253            qb.appendWhere(Mms.MESSAGE_BOX + "=" + msgBox);
254        }
255    }
256
257    @Override
258    public String getType(Uri uri) {
259        int match = sURLMatcher.match(uri);
260        switch (match) {
261            case MMS_ALL:
262            case MMS_INBOX:
263            case MMS_SENT:
264            case MMS_DRAFTS:
265            case MMS_OUTBOX:
266                return VND_ANDROID_DIR_MMS;
267            case MMS_ALL_ID:
268            case MMS_INBOX_ID:
269            case MMS_SENT_ID:
270            case MMS_DRAFTS_ID:
271            case MMS_OUTBOX_ID:
272                return VND_ANDROID_MMS;
273            case MMS_PART_ID: {
274                Cursor cursor = mOpenHelper.getReadableDatabase().query(
275                        TABLE_PART, new String[] { Part.CONTENT_TYPE },
276                        Part._ID + " = ?", new String[] { uri.getLastPathSegment() },
277                        null, null, null);
278                if (cursor != null) {
279                    try {
280                        if ((cursor.getCount() == 1) && cursor.moveToFirst()) {
281                            return cursor.getString(0);
282                        } else {
283                            Log.e(TAG, "cursor.count() != 1: " + uri);
284                        }
285                    } finally {
286                        cursor.close();
287                    }
288                } else {
289                    Log.e(TAG, "cursor == null: " + uri);
290                }
291                return "*/*";
292            }
293            case MMS_ALL_PART:
294            case MMS_MSG_PART:
295            case MMS_MSG_ADDR:
296            default:
297                return "*/*";
298        }
299    }
300
301    @Override
302    public Uri insert(Uri uri, ContentValues values) {
303        // Don't let anyone insert anything with the _data column
304        if (values != null && values.containsKey(Part._DATA)) {
305            return null;
306        }
307        final int callerUid = Binder.getCallingUid();
308        final String callerPkg = getCallingPackage();
309        int msgBox = Mms.MESSAGE_BOX_ALL;
310        boolean notify = true;
311
312        int match = sURLMatcher.match(uri);
313        if (LOCAL_LOGV) {
314            Log.v(TAG, "Insert uri=" + uri + ", match=" + match);
315        }
316
317        String table = TABLE_PDU;
318        switch (match) {
319            case MMS_ALL:
320                Object msgBoxObj = values.getAsInteger(Mms.MESSAGE_BOX);
321                if (msgBoxObj != null) {
322                    msgBox = (Integer) msgBoxObj;
323                }
324                else {
325                    // default to inbox
326                    msgBox = Mms.MESSAGE_BOX_INBOX;
327                }
328                break;
329            case MMS_INBOX:
330                msgBox = Mms.MESSAGE_BOX_INBOX;
331                break;
332            case MMS_SENT:
333                msgBox = Mms.MESSAGE_BOX_SENT;
334                break;
335            case MMS_DRAFTS:
336                msgBox = Mms.MESSAGE_BOX_DRAFTS;
337                break;
338            case MMS_OUTBOX:
339                msgBox = Mms.MESSAGE_BOX_OUTBOX;
340                break;
341            case MMS_MSG_PART:
342                notify = false;
343                table = TABLE_PART;
344                break;
345            case MMS_MSG_ADDR:
346                notify = false;
347                table = TABLE_ADDR;
348                break;
349            case MMS_SENDING_RATE:
350                notify = false;
351                table = TABLE_RATE;
352                break;
353            case MMS_DRM_STORAGE:
354                notify = false;
355                table = TABLE_DRM;
356                break;
357            default:
358                Log.e(TAG, "insert: invalid request: " + uri);
359                return null;
360        }
361
362        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
363        ContentValues finalValues;
364        Uri res = Mms.CONTENT_URI;
365        long rowId;
366
367        if (table.equals(TABLE_PDU)) {
368            boolean addDate = !values.containsKey(Mms.DATE);
369            boolean addMsgBox = !values.containsKey(Mms.MESSAGE_BOX);
370
371            // Filter keys we don't support yet.
372            filterUnsupportedKeys(values);
373
374            // TODO: Should initialValues be validated, e.g. if it
375            // missed some significant keys?
376            finalValues = new ContentValues(values);
377
378            long timeInMillis = System.currentTimeMillis();
379
380            if (addDate) {
381                finalValues.put(Mms.DATE, timeInMillis / 1000L);
382            }
383
384            if (addMsgBox && (msgBox != Mms.MESSAGE_BOX_ALL)) {
385                finalValues.put(Mms.MESSAGE_BOX, msgBox);
386            }
387
388            if (msgBox != Mms.MESSAGE_BOX_INBOX) {
389                // Mark all non-inbox messages read.
390                finalValues.put(Mms.READ, 1);
391            }
392
393            // thread_id
394            Long threadId = values.getAsLong(Mms.THREAD_ID);
395            String address = values.getAsString(CanonicalAddressesColumns.ADDRESS);
396
397            if (((threadId == null) || (threadId == 0)) && (!TextUtils.isEmpty(address))) {
398                finalValues.put(Mms.THREAD_ID, Threads.getOrCreateThreadId(getContext(), address));
399            }
400
401            if (ProviderUtil.shouldSetCreator(finalValues, callerUid)) {
402                // Only SYSTEM or PHONE can set CREATOR
403                // If caller is not SYSTEM or PHONE, or SYSTEM or PHONE does not set CREATOR
404                // set CREATOR using the truth on caller.
405                // Note: Inferring package name from UID may include unrelated package names
406                finalValues.put(Telephony.Mms.CREATOR, callerPkg);
407            }
408
409            if ((rowId = db.insert(table, null, finalValues)) <= 0) {
410                Log.e(TAG, "MmsProvider.insert: failed!");
411                return null;
412            }
413
414            res = Uri.parse(res + "/" + rowId);
415        } else if (table.equals(TABLE_ADDR)) {
416            finalValues = new ContentValues(values);
417            finalValues.put(Addr.MSG_ID, uri.getPathSegments().get(0));
418
419            if ((rowId = db.insert(table, null, finalValues)) <= 0) {
420                Log.e(TAG, "Failed to insert address");
421                return null;
422            }
423
424            res = Uri.parse(res + "/addr/" + rowId);
425        } else if (table.equals(TABLE_PART)) {
426            finalValues = new ContentValues(values);
427
428            if (match == MMS_MSG_PART) {
429                finalValues.put(Part.MSG_ID, uri.getPathSegments().get(0));
430            }
431
432            String contentType = values.getAsString("ct");
433
434            // text/plain and app application/smil store their "data" inline in the
435            // table so there's no need to create the file
436            boolean plainText = false;
437            boolean smilText = false;
438            if ("text/plain".equals(contentType)) {
439                plainText = true;
440            } else if ("application/smil".equals(contentType)) {
441                smilText = true;
442            }
443            if (!plainText && !smilText) {
444                // Use the filename if possible, otherwise use the current time as the name.
445                String contentLocation = values.getAsString("cl");
446                if (!TextUtils.isEmpty(contentLocation)) {
447                    File f = new File(contentLocation);
448                    contentLocation = "_" + f.getName();
449                } else {
450                    contentLocation = "";
451                }
452
453                // Generate the '_data' field of the part with default
454                // permission settings.
455                String path = getContext().getDir(PARTS_DIR_NAME, 0).getPath()
456                        + "/PART_" + System.currentTimeMillis() + contentLocation;
457
458                if (DownloadDrmHelper.isDrmConvertNeeded(contentType)) {
459                    // Adds the .fl extension to the filename if contentType is
460                    // "application/vnd.oma.drm.message"
461                    path = DownloadDrmHelper.modifyDrmFwLockFileExtension(path);
462                }
463
464                finalValues.put(Part._DATA, path);
465
466                File partFile = new File(path);
467                if (!partFile.exists()) {
468                    try {
469                        if (!partFile.createNewFile()) {
470                            throw new IllegalStateException(
471                                    "Unable to create new partFile: " + path);
472                        }
473                        // Give everyone rw permission until we encrypt the file
474                        // (in PduPersister.persistData). Once the file is encrypted, the
475                        // permissions will be set to 0644.
476                        int result = FileUtils.setPermissions(path, 0666, -1, -1);
477                        if (LOCAL_LOGV) {
478                            Log.d(TAG, "MmsProvider.insert setPermissions result: " + result);
479                        }
480                    } catch (IOException e) {
481                        Log.e(TAG, "createNewFile", e);
482                        throw new IllegalStateException(
483                                "Unable to create new partFile: " + path);
484                    }
485                }
486            }
487
488            if ((rowId = db.insert(table, null, finalValues)) <= 0) {
489                Log.e(TAG, "MmsProvider.insert: failed!");
490                return null;
491            }
492
493            res = Uri.parse(res + "/part/" + rowId);
494
495            // Don't use a trigger for updating the words table because of a bug
496            // in FTS3.  The bug is such that the call to get the last inserted
497            // row is incorrect.
498            if (plainText) {
499                // Update the words table with a corresponding row.  The words table
500                // allows us to search for words quickly, without scanning the whole
501                // table;
502                ContentValues cv = new ContentValues();
503
504                // we're using the row id of the part table row but we're also using ids
505                // from the sms table so this divides the space into two large chunks.
506                // The row ids from the part table start at 2 << 32.
507                cv.put(Telephony.MmsSms.WordsTable.ID, (2 << 32) + rowId);
508                cv.put(Telephony.MmsSms.WordsTable.INDEXED_TEXT, values.getAsString("text"));
509                cv.put(Telephony.MmsSms.WordsTable.SOURCE_ROW_ID, rowId);
510                cv.put(Telephony.MmsSms.WordsTable.TABLE_ID, 2);
511                db.insert(TABLE_WORDS, Telephony.MmsSms.WordsTable.INDEXED_TEXT, cv);
512            }
513
514        } else if (table.equals(TABLE_RATE)) {
515            long now = values.getAsLong(Rate.SENT_TIME);
516            long oneHourAgo = now - 1000 * 60 * 60;
517            // Delete all unused rows (time earlier than one hour ago).
518            db.delete(table, Rate.SENT_TIME + "<=" + oneHourAgo, null);
519            db.insert(table, null, values);
520        } else if (table.equals(TABLE_DRM)) {
521            String path = getContext().getDir(PARTS_DIR_NAME, 0).getPath()
522                    + "/PART_" + System.currentTimeMillis();
523            finalValues = new ContentValues(1);
524            finalValues.put("_data", path);
525
526            File partFile = new File(path);
527            if (!partFile.exists()) {
528                try {
529                    if (!partFile.createNewFile()) {
530                        throw new IllegalStateException(
531                                "Unable to create new file: " + path);
532                    }
533                } catch (IOException e) {
534                    Log.e(TAG, "createNewFile", e);
535                    throw new IllegalStateException(
536                            "Unable to create new file: " + path);
537                }
538            }
539
540            if ((rowId = db.insert(table, null, finalValues)) <= 0) {
541                Log.e(TAG, "MmsProvider.insert: failed!");
542                return null;
543            }
544            res = Uri.parse(res + "/drm/" + rowId);
545        } else {
546            throw new AssertionError("Unknown table type: " + table);
547        }
548
549        if (notify) {
550            notifyChange();
551        }
552        return res;
553    }
554
555    private int getMessageBoxByMatch(int match) {
556        switch (match) {
557            case MMS_INBOX_ID:
558            case MMS_INBOX:
559                return Mms.MESSAGE_BOX_INBOX;
560            case MMS_SENT_ID:
561            case MMS_SENT:
562                return Mms.MESSAGE_BOX_SENT;
563            case MMS_DRAFTS_ID:
564            case MMS_DRAFTS:
565                return Mms.MESSAGE_BOX_DRAFTS;
566            case MMS_OUTBOX_ID:
567            case MMS_OUTBOX:
568                return Mms.MESSAGE_BOX_OUTBOX;
569            default:
570                throw new IllegalArgumentException("bad Arg: " + match);
571        }
572    }
573
574    @Override
575    public int delete(Uri uri, String selection,
576            String[] selectionArgs) {
577        int match = sURLMatcher.match(uri);
578        if (LOCAL_LOGV) {
579            Log.v(TAG, "Delete uri=" + uri + ", match=" + match);
580        }
581
582        String table, extraSelection = null;
583        boolean notify = false;
584
585        switch (match) {
586            case MMS_ALL_ID:
587            case MMS_INBOX_ID:
588            case MMS_SENT_ID:
589            case MMS_DRAFTS_ID:
590            case MMS_OUTBOX_ID:
591                notify = true;
592                table = TABLE_PDU;
593                extraSelection = Mms._ID + "=" + uri.getLastPathSegment();
594                break;
595            case MMS_ALL:
596            case MMS_INBOX:
597            case MMS_SENT:
598            case MMS_DRAFTS:
599            case MMS_OUTBOX:
600                notify = true;
601                table = TABLE_PDU;
602                if (match != MMS_ALL) {
603                    int msgBox = getMessageBoxByMatch(match);
604                    extraSelection = Mms.MESSAGE_BOX + "=" + msgBox;
605                }
606                break;
607            case MMS_ALL_PART:
608                table = TABLE_PART;
609                break;
610            case MMS_MSG_PART:
611                table = TABLE_PART;
612                extraSelection = Part.MSG_ID + "=" + uri.getPathSegments().get(0);
613                break;
614            case MMS_PART_ID:
615                table = TABLE_PART;
616                extraSelection = Part._ID + "=" + uri.getPathSegments().get(1);
617                break;
618            case MMS_MSG_ADDR:
619                table = TABLE_ADDR;
620                extraSelection = Addr.MSG_ID + "=" + uri.getPathSegments().get(0);
621                break;
622            case MMS_DRM_STORAGE:
623                table = TABLE_DRM;
624                break;
625            default:
626                Log.w(TAG, "No match for URI '" + uri + "'");
627                return 0;
628        }
629
630        String finalSelection = concatSelections(selection, extraSelection);
631        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
632        int deletedRows = 0;
633
634        if (TABLE_PDU.equals(table)) {
635            deletedRows = deleteMessages(getContext(), db, finalSelection,
636                                         selectionArgs, uri);
637        } else if (TABLE_PART.equals(table)) {
638            deletedRows = deleteParts(db, finalSelection, selectionArgs);
639        } else if (TABLE_DRM.equals(table)) {
640            deletedRows = deleteTempDrmData(db, finalSelection, selectionArgs);
641        } else {
642            deletedRows = db.delete(table, finalSelection, selectionArgs);
643        }
644
645        if ((deletedRows > 0) && notify) {
646            notifyChange();
647        }
648        return deletedRows;
649    }
650
651    static int deleteMessages(Context context, SQLiteDatabase db,
652            String selection, String[] selectionArgs, Uri uri) {
653        Cursor cursor = db.query(TABLE_PDU, new String[] { Mms._ID },
654                selection, selectionArgs, null, null, null);
655        if (cursor == null) {
656            return 0;
657        }
658
659        try {
660            if (cursor.getCount() == 0) {
661                return 0;
662            }
663
664            while (cursor.moveToNext()) {
665                deleteParts(db, Part.MSG_ID + " = ?",
666                        new String[] { String.valueOf(cursor.getLong(0)) });
667            }
668        } finally {
669            cursor.close();
670        }
671
672        int count = db.delete(TABLE_PDU, selection, selectionArgs);
673        if (count > 0) {
674            Intent intent = new Intent(Mms.Intents.CONTENT_CHANGED_ACTION);
675            intent.putExtra(Mms.Intents.DELETED_CONTENTS, uri);
676            if (LOCAL_LOGV) {
677                Log.v(TAG, "Broadcasting intent: " + intent);
678            }
679            context.sendBroadcast(intent);
680        }
681        return count;
682    }
683
684    private static int deleteParts(SQLiteDatabase db, String selection,
685            String[] selectionArgs) {
686        return deleteDataRows(db, TABLE_PART, selection, selectionArgs);
687    }
688
689    private static int deleteTempDrmData(SQLiteDatabase db, String selection,
690            String[] selectionArgs) {
691        return deleteDataRows(db, TABLE_DRM, selection, selectionArgs);
692    }
693
694    private static int deleteDataRows(SQLiteDatabase db, String table,
695            String selection, String[] selectionArgs) {
696        Cursor cursor = db.query(table, new String[] { "_data" },
697                selection, selectionArgs, null, null, null);
698        if (cursor == null) {
699            // FIXME: This might be an error, ignore it may cause
700            // unpredictable result.
701            return 0;
702        }
703
704        try {
705            if (cursor.getCount() == 0) {
706                return 0;
707            }
708
709            while (cursor.moveToNext()) {
710                try {
711                    // Delete the associated files saved on file-system.
712                    String path = cursor.getString(0);
713                    if (path != null) {
714                        new File(path).delete();
715                    }
716                } catch (Throwable ex) {
717                    Log.e(TAG, ex.getMessage(), ex);
718                }
719            }
720        } finally {
721            cursor.close();
722        }
723
724        return db.delete(table, selection, selectionArgs);
725    }
726
727    @Override
728    public int update(Uri uri, ContentValues values,
729            String selection, String[] selectionArgs) {
730        // Don't let anyone update the _data column
731        if (values != null && values.containsKey(Part._DATA)) {
732            return 0;
733        }
734        final int callerUid = Binder.getCallingUid();
735        final String callerPkg = getCallingPackage();
736        int match = sURLMatcher.match(uri);
737        if (LOCAL_LOGV) {
738            Log.v(TAG, "Update uri=" + uri + ", match=" + match);
739        }
740
741        boolean notify = false;
742        String msgId = null;
743        String table;
744
745        switch (match) {
746            case MMS_ALL_ID:
747            case MMS_INBOX_ID:
748            case MMS_SENT_ID:
749            case MMS_DRAFTS_ID:
750            case MMS_OUTBOX_ID:
751                msgId = uri.getLastPathSegment();
752            // fall-through
753            case MMS_ALL:
754            case MMS_INBOX:
755            case MMS_SENT:
756            case MMS_DRAFTS:
757            case MMS_OUTBOX:
758                notify = true;
759                table = TABLE_PDU;
760                break;
761
762            case MMS_MSG_PART:
763            case MMS_PART_ID:
764                table = TABLE_PART;
765                break;
766
767            case MMS_PART_RESET_FILE_PERMISSION:
768                String path = getContext().getDir(PARTS_DIR_NAME, 0).getPath() + '/' +
769                        uri.getPathSegments().get(1);
770                // Reset the file permission back to read for everyone but me.
771                int result = FileUtils.setPermissions(path, 0644, -1, -1);
772                if (LOCAL_LOGV) {
773                    Log.d(TAG, "MmsProvider.update setPermissions result: " + result +
774                            " for path: " + path);
775                }
776                return 0;
777
778            default:
779                Log.w(TAG, "Update operation for '" + uri + "' not implemented.");
780                return 0;
781        }
782
783        String extraSelection = null;
784        ContentValues finalValues;
785        if (table.equals(TABLE_PDU)) {
786            // Filter keys that we don't support yet.
787            filterUnsupportedKeys(values);
788            if (ProviderUtil.shouldRemoveCreator(values, callerUid)) {
789                // CREATOR should not be changed by non-SYSTEM/PHONE apps
790                Log.w(TAG, callerPkg + " tries to update CREATOR");
791                values.remove(Mms.CREATOR);
792            }
793            finalValues = new ContentValues(values);
794
795            if (msgId != null) {
796                extraSelection = Mms._ID + "=" + msgId;
797            }
798        } else if (table.equals(TABLE_PART)) {
799            finalValues = new ContentValues(values);
800
801            switch (match) {
802                case MMS_MSG_PART:
803                    extraSelection = Part.MSG_ID + "=" + uri.getPathSegments().get(0);
804                    break;
805                case MMS_PART_ID:
806                    extraSelection = Part._ID + "=" + uri.getPathSegments().get(1);
807                    break;
808                default:
809                    break;
810            }
811        } else {
812            return 0;
813        }
814
815        String finalSelection = concatSelections(selection, extraSelection);
816        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
817        int count = db.update(table, finalValues, finalSelection, selectionArgs);
818        if (notify && (count > 0)) {
819            notifyChange();
820        }
821        return count;
822    }
823
824    @Override
825    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
826        int match = sURLMatcher.match(uri);
827
828        if (Log.isLoggable(TAG, Log.VERBOSE)) {
829            Log.d(TAG, "openFile: uri=" + uri + ", mode=" + mode + ", match=" + match);
830        }
831
832        if (match != MMS_PART_ID) {
833            return null;
834        }
835
836        // Verify that the _data path points to mms data
837        Cursor c = query(uri, new String[]{"_data"}, null, null, null);
838        int count = (c != null) ? c.getCount() : 0;
839        if (count != 1) {
840            // If there is not exactly one result, throw an appropriate
841            // exception.
842            if (c != null) {
843                c.close();
844            }
845            if (count == 0) {
846                throw new FileNotFoundException("No entry for " + uri);
847            }
848            throw new FileNotFoundException("Multiple items at " + uri);
849        }
850
851        c.moveToFirst();
852        int i = c.getColumnIndex("_data");
853        String path = (i >= 0 ? c.getString(i) : null);
854        c.close();
855
856        if (path == null) {
857            return null;
858        }
859        try {
860            File filePath = new File(path);
861            if (!filePath.getCanonicalPath()
862                    .startsWith(getContext().getDir(PARTS_DIR_NAME, 0).getCanonicalPath())) {
863                Log.e(TAG, "openFile: path "
864                        + filePath.getCanonicalPath()
865                        + " does not start with "
866                        + getContext().getDir(PARTS_DIR_NAME, 0).getCanonicalPath());
867                // Don't care return value
868                filePath.delete();
869                return null;
870            }
871        } catch (IOException e) {
872            Log.e(TAG, "openFile: create path failed " + e, e);
873            return null;
874        }
875
876        return openFileHelper(uri, mode);
877    }
878
879    private void filterUnsupportedKeys(ContentValues values) {
880        // Some columns are unsupported.  They should therefore
881        // neither be inserted nor updated.  Filter them out.
882        values.remove(Mms.DELIVERY_TIME_TOKEN);
883        values.remove(Mms.SENDER_VISIBILITY);
884        values.remove(Mms.REPLY_CHARGING);
885        values.remove(Mms.REPLY_CHARGING_DEADLINE_TOKEN);
886        values.remove(Mms.REPLY_CHARGING_DEADLINE);
887        values.remove(Mms.REPLY_CHARGING_ID);
888        values.remove(Mms.REPLY_CHARGING_SIZE);
889        values.remove(Mms.PREVIOUSLY_SENT_BY);
890        values.remove(Mms.PREVIOUSLY_SENT_DATE);
891        values.remove(Mms.STORE);
892        values.remove(Mms.MM_STATE);
893        values.remove(Mms.MM_FLAGS_TOKEN);
894        values.remove(Mms.MM_FLAGS);
895        values.remove(Mms.STORE_STATUS);
896        values.remove(Mms.STORE_STATUS_TEXT);
897        values.remove(Mms.STORED);
898        values.remove(Mms.TOTALS);
899        values.remove(Mms.MBOX_TOTALS);
900        values.remove(Mms.MBOX_TOTALS_TOKEN);
901        values.remove(Mms.QUOTAS);
902        values.remove(Mms.MBOX_QUOTAS);
903        values.remove(Mms.MBOX_QUOTAS_TOKEN);
904        values.remove(Mms.MESSAGE_COUNT);
905        values.remove(Mms.START);
906        values.remove(Mms.DISTRIBUTION_INDICATOR);
907        values.remove(Mms.ELEMENT_DESCRIPTOR);
908        values.remove(Mms.LIMIT);
909        values.remove(Mms.RECOMMENDED_RETRIEVAL_MODE);
910        values.remove(Mms.RECOMMENDED_RETRIEVAL_MODE_TEXT);
911        values.remove(Mms.STATUS_TEXT);
912        values.remove(Mms.APPLIC_ID);
913        values.remove(Mms.REPLY_APPLIC_ID);
914        values.remove(Mms.AUX_APPLIC_ID);
915        values.remove(Mms.DRM_CONTENT);
916        values.remove(Mms.ADAPTATION_ALLOWED);
917        values.remove(Mms.REPLACE_ID);
918        values.remove(Mms.CANCEL_ID);
919        values.remove(Mms.CANCEL_STATUS);
920
921        // Keys shouldn't be inserted or updated.
922        values.remove(Mms._ID);
923    }
924
925    private void notifyChange() {
926        getContext().getContentResolver().notifyChange(
927                MmsSms.CONTENT_URI, null, true, UserHandle.USER_ALL);
928    }
929
930    private final static String TAG = "MmsProvider";
931    private final static String VND_ANDROID_MMS = "vnd.android/mms";
932    private final static String VND_ANDROID_DIR_MMS = "vnd.android-dir/mms";
933    private final static boolean DEBUG = false;
934    private final static boolean LOCAL_LOGV = false;
935
936    private static final int MMS_ALL                      = 0;
937    private static final int MMS_ALL_ID                   = 1;
938    private static final int MMS_INBOX                    = 2;
939    private static final int MMS_INBOX_ID                 = 3;
940    private static final int MMS_SENT                     = 4;
941    private static final int MMS_SENT_ID                  = 5;
942    private static final int MMS_DRAFTS                   = 6;
943    private static final int MMS_DRAFTS_ID                = 7;
944    private static final int MMS_OUTBOX                   = 8;
945    private static final int MMS_OUTBOX_ID                = 9;
946    private static final int MMS_ALL_PART                 = 10;
947    private static final int MMS_MSG_PART                 = 11;
948    private static final int MMS_PART_ID                  = 12;
949    private static final int MMS_MSG_ADDR                 = 13;
950    private static final int MMS_SENDING_RATE             = 14;
951    private static final int MMS_REPORT_STATUS            = 15;
952    private static final int MMS_REPORT_REQUEST           = 16;
953    private static final int MMS_DRM_STORAGE              = 17;
954    private static final int MMS_DRM_STORAGE_ID           = 18;
955    private static final int MMS_THREADS                  = 19;
956    private static final int MMS_PART_RESET_FILE_PERMISSION = 20;
957
958    private static final UriMatcher
959            sURLMatcher = new UriMatcher(UriMatcher.NO_MATCH);
960
961    static {
962        sURLMatcher.addURI("mms", null,         MMS_ALL);
963        sURLMatcher.addURI("mms", "#",          MMS_ALL_ID);
964        sURLMatcher.addURI("mms", "inbox",      MMS_INBOX);
965        sURLMatcher.addURI("mms", "inbox/#",    MMS_INBOX_ID);
966        sURLMatcher.addURI("mms", "sent",       MMS_SENT);
967        sURLMatcher.addURI("mms", "sent/#",     MMS_SENT_ID);
968        sURLMatcher.addURI("mms", "drafts",     MMS_DRAFTS);
969        sURLMatcher.addURI("mms", "drafts/#",   MMS_DRAFTS_ID);
970        sURLMatcher.addURI("mms", "outbox",     MMS_OUTBOX);
971        sURLMatcher.addURI("mms", "outbox/#",   MMS_OUTBOX_ID);
972        sURLMatcher.addURI("mms", "part",       MMS_ALL_PART);
973        sURLMatcher.addURI("mms", "#/part",     MMS_MSG_PART);
974        sURLMatcher.addURI("mms", "part/#",     MMS_PART_ID);
975        sURLMatcher.addURI("mms", "#/addr",     MMS_MSG_ADDR);
976        sURLMatcher.addURI("mms", "rate",       MMS_SENDING_RATE);
977        sURLMatcher.addURI("mms", "report-status/#",  MMS_REPORT_STATUS);
978        sURLMatcher.addURI("mms", "report-request/#", MMS_REPORT_REQUEST);
979        sURLMatcher.addURI("mms", "drm",        MMS_DRM_STORAGE);
980        sURLMatcher.addURI("mms", "drm/#",      MMS_DRM_STORAGE_ID);
981        sURLMatcher.addURI("mms", "threads",    MMS_THREADS);
982        sURLMatcher.addURI("mms", "resetFilePerm/*",    MMS_PART_RESET_FILE_PERMISSION);
983    }
984
985    private SQLiteOpenHelper mOpenHelper;
986
987    private static String concatSelections(String selection1, String selection2) {
988        if (TextUtils.isEmpty(selection1)) {
989            return selection2;
990        } else if (TextUtils.isEmpty(selection2)) {
991            return selection1;
992        } else {
993            return selection1 + " AND " + selection2;
994        }
995    }
996}
997
998