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