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