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