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