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