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