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