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