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