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