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 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 */ 85 public static String getPduTable(boolean accessRestricted) { 86 return accessRestricted ? VIEW_PDU_RESTRICTED : TABLE_PDU; 87 } 88 89 @Override 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 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 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 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 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 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 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 690 private static int deleteParts(SQLiteDatabase db, String selection, 691 String[] selectionArgs) { 692 return deleteDataRows(db, TABLE_PART, selection, selectionArgs); 693 } 694 695 private static int deleteTempDrmData(SQLiteDatabase db, String selection, 696 String[] selectionArgs) { 697 return deleteDataRows(db, TABLE_DRM, selection, selectionArgs); 698 } 699 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 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 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 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 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 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 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