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