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