1 /* 2 * Copyright (C) 2007-2008 Esmertec AG. 3 * Copyright (C) 2007-2008 The Android Open Source Project 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package com.google.android.mms.pdu; 19 20 import com.google.android.mms.ContentType; 21 import com.google.android.mms.InvalidHeaderValueException; 22 import com.google.android.mms.MmsException; 23 import com.google.android.mms.util.DownloadDrmHelper; 24 import com.google.android.mms.util.DrmConvertSession; 25 import com.google.android.mms.util.PduCache; 26 import com.google.android.mms.util.PduCacheEntry; 27 import com.google.android.mms.util.SqliteWrapper; 28 29 import android.content.ContentResolver; 30 import android.content.ContentUris; 31 import android.content.ContentValues; 32 import android.content.Context; 33 import android.database.Cursor; 34 import android.database.DatabaseUtils; 35 import android.database.sqlite.SQLiteException; 36 import android.drm.DrmManagerClient; 37 import android.net.Uri; 38 import android.provider.MediaStore; 39 import android.provider.Telephony; 40 import android.provider.Telephony.Mms; 41 import android.provider.Telephony.MmsSms; 42 import android.provider.Telephony.Threads; 43 import android.provider.Telephony.Mms.Addr; 44 import android.provider.Telephony.Mms.Part; 45 import android.provider.Telephony.MmsSms.PendingMessages; 46 import android.telephony.PhoneNumberUtils; 47 import android.telephony.TelephonyManager; 48 import android.text.TextUtils; 49 import android.util.Log; 50 51 import java.io.ByteArrayOutputStream; 52 import java.io.File; 53 import java.io.FileNotFoundException; 54 import java.io.IOException; 55 import java.io.InputStream; 56 import java.io.OutputStream; 57 import java.io.UnsupportedEncodingException; 58 import java.util.ArrayList; 59 import java.util.HashMap; 60 import java.util.HashSet; 61 import java.util.Map; 62 import java.util.Set; 63 import java.util.Map.Entry; 64 65 import com.google.android.mms.pdu.EncodedStringValue; 66 67 /** 68 * This class is the high-level manager of PDU storage. 69 */ 70 public class PduPersister { 71 private static final String TAG = "PduPersister"; 72 private static final boolean DEBUG = false; 73 private static final boolean LOCAL_LOGV = false; 74 75 private static final long DUMMY_THREAD_ID = Long.MAX_VALUE; 76 77 /** 78 * The uri of temporary drm objects. 79 */ 80 public static final String TEMPORARY_DRM_OBJECT_URI = 81 "content://mms/" + Long.MAX_VALUE + "/part"; 82 /** 83 * Indicate that we transiently failed to process a MM. 84 */ 85 public static final int PROC_STATUS_TRANSIENT_FAILURE = 1; 86 /** 87 * Indicate that we permanently failed to process a MM. 88 */ 89 public static final int PROC_STATUS_PERMANENTLY_FAILURE = 2; 90 /** 91 * Indicate that we have successfully processed a MM. 92 */ 93 public static final int PROC_STATUS_COMPLETED = 3; 94 95 private static PduPersister sPersister; 96 private static final PduCache PDU_CACHE_INSTANCE; 97 98 private static final int[] ADDRESS_FIELDS = new int[] { 99 PduHeaders.BCC, 100 PduHeaders.CC, 101 PduHeaders.FROM, 102 PduHeaders.TO 103 }; 104 105 private static final String[] PDU_PROJECTION = new String[] { 106 Mms._ID, 107 Mms.MESSAGE_BOX, 108 Mms.THREAD_ID, 109 Mms.RETRIEVE_TEXT, 110 Mms.SUBJECT, 111 Mms.CONTENT_LOCATION, 112 Mms.CONTENT_TYPE, 113 Mms.MESSAGE_CLASS, 114 Mms.MESSAGE_ID, 115 Mms.RESPONSE_TEXT, 116 Mms.TRANSACTION_ID, 117 Mms.CONTENT_CLASS, 118 Mms.DELIVERY_REPORT, 119 Mms.MESSAGE_TYPE, 120 Mms.MMS_VERSION, 121 Mms.PRIORITY, 122 Mms.READ_REPORT, 123 Mms.READ_STATUS, 124 Mms.REPORT_ALLOWED, 125 Mms.RETRIEVE_STATUS, 126 Mms.STATUS, 127 Mms.DATE, 128 Mms.DELIVERY_TIME, 129 Mms.EXPIRY, 130 Mms.MESSAGE_SIZE, 131 Mms.SUBJECT_CHARSET, 132 Mms.RETRIEVE_TEXT_CHARSET, 133 }; 134 135 private static final int PDU_COLUMN_ID = 0; 136 private static final int PDU_COLUMN_MESSAGE_BOX = 1; 137 private static final int PDU_COLUMN_THREAD_ID = 2; 138 private static final int PDU_COLUMN_RETRIEVE_TEXT = 3; 139 private static final int PDU_COLUMN_SUBJECT = 4; 140 private static final int PDU_COLUMN_CONTENT_LOCATION = 5; 141 private static final int PDU_COLUMN_CONTENT_TYPE = 6; 142 private static final int PDU_COLUMN_MESSAGE_CLASS = 7; 143 private static final int PDU_COLUMN_MESSAGE_ID = 8; 144 private static final int PDU_COLUMN_RESPONSE_TEXT = 9; 145 private static final int PDU_COLUMN_TRANSACTION_ID = 10; 146 private static final int PDU_COLUMN_CONTENT_CLASS = 11; 147 private static final int PDU_COLUMN_DELIVERY_REPORT = 12; 148 private static final int PDU_COLUMN_MESSAGE_TYPE = 13; 149 private static final int PDU_COLUMN_MMS_VERSION = 14; 150 private static final int PDU_COLUMN_PRIORITY = 15; 151 private static final int PDU_COLUMN_READ_REPORT = 16; 152 private static final int PDU_COLUMN_READ_STATUS = 17; 153 private static final int PDU_COLUMN_REPORT_ALLOWED = 18; 154 private static final int PDU_COLUMN_RETRIEVE_STATUS = 19; 155 private static final int PDU_COLUMN_STATUS = 20; 156 private static final int PDU_COLUMN_DATE = 21; 157 private static final int PDU_COLUMN_DELIVERY_TIME = 22; 158 private static final int PDU_COLUMN_EXPIRY = 23; 159 private static final int PDU_COLUMN_MESSAGE_SIZE = 24; 160 private static final int PDU_COLUMN_SUBJECT_CHARSET = 25; 161 private static final int PDU_COLUMN_RETRIEVE_TEXT_CHARSET = 26; 162 163 private static final String[] PART_PROJECTION = new String[] { 164 Part._ID, 165 Part.CHARSET, 166 Part.CONTENT_DISPOSITION, 167 Part.CONTENT_ID, 168 Part.CONTENT_LOCATION, 169 Part.CONTENT_TYPE, 170 Part.FILENAME, 171 Part.NAME, 172 Part.TEXT 173 }; 174 175 private static final int PART_COLUMN_ID = 0; 176 private static final int PART_COLUMN_CHARSET = 1; 177 private static final int PART_COLUMN_CONTENT_DISPOSITION = 2; 178 private static final int PART_COLUMN_CONTENT_ID = 3; 179 private static final int PART_COLUMN_CONTENT_LOCATION = 4; 180 private static final int PART_COLUMN_CONTENT_TYPE = 5; 181 private static final int PART_COLUMN_FILENAME = 6; 182 private static final int PART_COLUMN_NAME = 7; 183 private static final int PART_COLUMN_TEXT = 8; 184 185 private static final HashMap<Uri, Integer> MESSAGE_BOX_MAP; 186 // These map are used for convenience in persist() and load(). 187 private static final HashMap<Integer, Integer> CHARSET_COLUMN_INDEX_MAP; 188 private static final HashMap<Integer, Integer> ENCODED_STRING_COLUMN_INDEX_MAP; 189 private static final HashMap<Integer, Integer> TEXT_STRING_COLUMN_INDEX_MAP; 190 private static final HashMap<Integer, Integer> OCTET_COLUMN_INDEX_MAP; 191 private static final HashMap<Integer, Integer> LONG_COLUMN_INDEX_MAP; 192 private static final HashMap<Integer, String> CHARSET_COLUMN_NAME_MAP; 193 private static final HashMap<Integer, String> ENCODED_STRING_COLUMN_NAME_MAP; 194 private static final HashMap<Integer, String> TEXT_STRING_COLUMN_NAME_MAP; 195 private static final HashMap<Integer, String> OCTET_COLUMN_NAME_MAP; 196 private static final HashMap<Integer, String> LONG_COLUMN_NAME_MAP; 197 198 static { 199 MESSAGE_BOX_MAP = new HashMap<Uri, Integer>(); 200 MESSAGE_BOX_MAP.put(Mms.Inbox.CONTENT_URI, Mms.MESSAGE_BOX_INBOX); 201 MESSAGE_BOX_MAP.put(Mms.Sent.CONTENT_URI, Mms.MESSAGE_BOX_SENT); 202 MESSAGE_BOX_MAP.put(Mms.Draft.CONTENT_URI, Mms.MESSAGE_BOX_DRAFTS); 203 MESSAGE_BOX_MAP.put(Mms.Outbox.CONTENT_URI, Mms.MESSAGE_BOX_OUTBOX); 204 205 CHARSET_COLUMN_INDEX_MAP = new HashMap<Integer, Integer>(); 206 CHARSET_COLUMN_INDEX_MAP.put(PduHeaders.SUBJECT, PDU_COLUMN_SUBJECT_CHARSET); 207 CHARSET_COLUMN_INDEX_MAP.put(PduHeaders.RETRIEVE_TEXT, PDU_COLUMN_RETRIEVE_TEXT_CHARSET); 208 209 CHARSET_COLUMN_NAME_MAP = new HashMap<Integer, String>(); 210 CHARSET_COLUMN_NAME_MAP.put(PduHeaders.SUBJECT, Mms.SUBJECT_CHARSET); 211 CHARSET_COLUMN_NAME_MAP.put(PduHeaders.RETRIEVE_TEXT, Mms.RETRIEVE_TEXT_CHARSET); 212 213 // Encoded string field code -> column index/name map. 214 ENCODED_STRING_COLUMN_INDEX_MAP = new HashMap<Integer, Integer>(); 215 ENCODED_STRING_COLUMN_INDEX_MAP.put(PduHeaders.RETRIEVE_TEXT, PDU_COLUMN_RETRIEVE_TEXT); 216 ENCODED_STRING_COLUMN_INDEX_MAP.put(PduHeaders.SUBJECT, PDU_COLUMN_SUBJECT); 217 218 ENCODED_STRING_COLUMN_NAME_MAP = new HashMap<Integer, String>(); 219 ENCODED_STRING_COLUMN_NAME_MAP.put(PduHeaders.RETRIEVE_TEXT, Mms.RETRIEVE_TEXT); 220 ENCODED_STRING_COLUMN_NAME_MAP.put(PduHeaders.SUBJECT, Mms.SUBJECT); 221 222 // Text string field code -> column index/name map. 223 TEXT_STRING_COLUMN_INDEX_MAP = new HashMap<Integer, Integer>(); 224 TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.CONTENT_LOCATION, PDU_COLUMN_CONTENT_LOCATION); 225 TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.CONTENT_TYPE, PDU_COLUMN_CONTENT_TYPE); 226 TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.MESSAGE_CLASS, PDU_COLUMN_MESSAGE_CLASS); 227 TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.MESSAGE_ID, PDU_COLUMN_MESSAGE_ID); 228 TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.RESPONSE_TEXT, PDU_COLUMN_RESPONSE_TEXT); 229 TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.TRANSACTION_ID, PDU_COLUMN_TRANSACTION_ID); 230 231 TEXT_STRING_COLUMN_NAME_MAP = new HashMap<Integer, String>(); 232 TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.CONTENT_LOCATION, Mms.CONTENT_LOCATION); 233 TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.CONTENT_TYPE, Mms.CONTENT_TYPE); 234 TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_CLASS, Mms.MESSAGE_CLASS); 235 TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_ID, Mms.MESSAGE_ID); 236 TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.RESPONSE_TEXT, Mms.RESPONSE_TEXT); 237 TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.TRANSACTION_ID, Mms.TRANSACTION_ID); 238 239 // Octet field code -> column index/name map. 240 OCTET_COLUMN_INDEX_MAP = new HashMap<Integer, Integer>(); 241 OCTET_COLUMN_INDEX_MAP.put(PduHeaders.CONTENT_CLASS, PDU_COLUMN_CONTENT_CLASS); 242 OCTET_COLUMN_INDEX_MAP.put(PduHeaders.DELIVERY_REPORT, PDU_COLUMN_DELIVERY_REPORT); 243 OCTET_COLUMN_INDEX_MAP.put(PduHeaders.MESSAGE_TYPE, PDU_COLUMN_MESSAGE_TYPE); 244 OCTET_COLUMN_INDEX_MAP.put(PduHeaders.MMS_VERSION, PDU_COLUMN_MMS_VERSION); 245 OCTET_COLUMN_INDEX_MAP.put(PduHeaders.PRIORITY, PDU_COLUMN_PRIORITY); 246 OCTET_COLUMN_INDEX_MAP.put(PduHeaders.READ_REPORT, PDU_COLUMN_READ_REPORT); 247 OCTET_COLUMN_INDEX_MAP.put(PduHeaders.READ_STATUS, PDU_COLUMN_READ_STATUS); 248 OCTET_COLUMN_INDEX_MAP.put(PduHeaders.REPORT_ALLOWED, PDU_COLUMN_REPORT_ALLOWED); 249 OCTET_COLUMN_INDEX_MAP.put(PduHeaders.RETRIEVE_STATUS, PDU_COLUMN_RETRIEVE_STATUS); 250 OCTET_COLUMN_INDEX_MAP.put(PduHeaders.STATUS, PDU_COLUMN_STATUS); 251 252 OCTET_COLUMN_NAME_MAP = new HashMap<Integer, String>(); 253 OCTET_COLUMN_NAME_MAP.put(PduHeaders.CONTENT_CLASS, Mms.CONTENT_CLASS); 254 OCTET_COLUMN_NAME_MAP.put(PduHeaders.DELIVERY_REPORT, Mms.DELIVERY_REPORT); 255 OCTET_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_TYPE, Mms.MESSAGE_TYPE); 256 OCTET_COLUMN_NAME_MAP.put(PduHeaders.MMS_VERSION, Mms.MMS_VERSION); 257 OCTET_COLUMN_NAME_MAP.put(PduHeaders.PRIORITY, Mms.PRIORITY); 258 OCTET_COLUMN_NAME_MAP.put(PduHeaders.READ_REPORT, Mms.READ_REPORT); 259 OCTET_COLUMN_NAME_MAP.put(PduHeaders.READ_STATUS, Mms.READ_STATUS); 260 OCTET_COLUMN_NAME_MAP.put(PduHeaders.REPORT_ALLOWED, Mms.REPORT_ALLOWED); 261 OCTET_COLUMN_NAME_MAP.put(PduHeaders.RETRIEVE_STATUS, Mms.RETRIEVE_STATUS); 262 OCTET_COLUMN_NAME_MAP.put(PduHeaders.STATUS, Mms.STATUS); 263 264 // Long field code -> column index/name map. 265 LONG_COLUMN_INDEX_MAP = new HashMap<Integer, Integer>(); 266 LONG_COLUMN_INDEX_MAP.put(PduHeaders.DATE, PDU_COLUMN_DATE); 267 LONG_COLUMN_INDEX_MAP.put(PduHeaders.DELIVERY_TIME, PDU_COLUMN_DELIVERY_TIME); 268 LONG_COLUMN_INDEX_MAP.put(PduHeaders.EXPIRY, PDU_COLUMN_EXPIRY); 269 LONG_COLUMN_INDEX_MAP.put(PduHeaders.MESSAGE_SIZE, PDU_COLUMN_MESSAGE_SIZE); 270 271 LONG_COLUMN_NAME_MAP = new HashMap<Integer, String>(); 272 LONG_COLUMN_NAME_MAP.put(PduHeaders.DATE, Mms.DATE); 273 LONG_COLUMN_NAME_MAP.put(PduHeaders.DELIVERY_TIME, Mms.DELIVERY_TIME); 274 LONG_COLUMN_NAME_MAP.put(PduHeaders.EXPIRY, Mms.EXPIRY); 275 LONG_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_SIZE, Mms.MESSAGE_SIZE); 276 277 PDU_CACHE_INSTANCE = PduCache.getInstance(); 278 } 279 280 private final Context mContext; 281 private final ContentResolver mContentResolver; 282 private final DrmManagerClient mDrmManagerClient; 283 private final TelephonyManager mTelephonyManager; 284 285 private PduPersister(Context context) { 286 mContext = context; 287 mContentResolver = context.getContentResolver(); 288 mDrmManagerClient = new DrmManagerClient(context); 289 mTelephonyManager = (TelephonyManager)context 290 .getSystemService(Context.TELEPHONY_SERVICE); 291 } 292 293 /** Get(or create if not exist) an instance of PduPersister */ 294 public static PduPersister getPduPersister(Context context) { 295 if ((sPersister == null) || !context.equals(sPersister.mContext)) { 296 sPersister = new PduPersister(context); 297 } 298 299 return sPersister; 300 } 301 302 private void setEncodedStringValueToHeaders( 303 Cursor c, int columnIndex, 304 PduHeaders headers, int mapColumn) { 305 String s = c.getString(columnIndex); 306 if ((s != null) && (s.length() > 0)) { 307 int charsetColumnIndex = CHARSET_COLUMN_INDEX_MAP.get(mapColumn); 308 int charset = c.getInt(charsetColumnIndex); 309 EncodedStringValue value = new EncodedStringValue( 310 charset, getBytes(s)); 311 headers.setEncodedStringValue(value, mapColumn); 312 } 313 } 314 315 private void setTextStringToHeaders( 316 Cursor c, int columnIndex, 317 PduHeaders headers, int mapColumn) { 318 String s = c.getString(columnIndex); 319 if (s != null) { 320 headers.setTextString(getBytes(s), mapColumn); 321 } 322 } 323 324 private void setOctetToHeaders( 325 Cursor c, int columnIndex, 326 PduHeaders headers, int mapColumn) throws InvalidHeaderValueException { 327 if (!c.isNull(columnIndex)) { 328 int b = c.getInt(columnIndex); 329 headers.setOctet(b, mapColumn); 330 } 331 } 332 333 private void setLongToHeaders( 334 Cursor c, int columnIndex, 335 PduHeaders headers, int mapColumn) { 336 if (!c.isNull(columnIndex)) { 337 long l = c.getLong(columnIndex); 338 headers.setLongInteger(l, mapColumn); 339 } 340 } 341 342 private Integer getIntegerFromPartColumn(Cursor c, int columnIndex) { 343 if (!c.isNull(columnIndex)) { 344 return c.getInt(columnIndex); 345 } 346 return null; 347 } 348 349 private byte[] getByteArrayFromPartColumn(Cursor c, int columnIndex) { 350 if (!c.isNull(columnIndex)) { 351 return getBytes(c.getString(columnIndex)); 352 } 353 return null; 354 } 355 356 private PduPart[] loadParts(long msgId) throws MmsException { 357 Cursor c = SqliteWrapper.query(mContext, mContentResolver, 358 Uri.parse("content://mms/" + msgId + "/part"), 359 PART_PROJECTION, null, null, null); 360 361 PduPart[] parts = null; 362 363 try { 364 if ((c == null) || (c.getCount() == 0)) { 365 if (LOCAL_LOGV) { 366 Log.v(TAG, "loadParts(" + msgId + "): no part to load."); 367 } 368 return null; 369 } 370 371 int partCount = c.getCount(); 372 int partIdx = 0; 373 parts = new PduPart[partCount]; 374 while (c.moveToNext()) { 375 PduPart part = new PduPart(); 376 Integer charset = getIntegerFromPartColumn( 377 c, PART_COLUMN_CHARSET); 378 if (charset != null) { 379 part.setCharset(charset); 380 } 381 382 byte[] contentDisposition = getByteArrayFromPartColumn( 383 c, PART_COLUMN_CONTENT_DISPOSITION); 384 if (contentDisposition != null) { 385 part.setContentDisposition(contentDisposition); 386 } 387 388 byte[] contentId = getByteArrayFromPartColumn( 389 c, PART_COLUMN_CONTENT_ID); 390 if (contentId != null) { 391 part.setContentId(contentId); 392 } 393 394 byte[] contentLocation = getByteArrayFromPartColumn( 395 c, PART_COLUMN_CONTENT_LOCATION); 396 if (contentLocation != null) { 397 part.setContentLocation(contentLocation); 398 } 399 400 byte[] contentType = getByteArrayFromPartColumn( 401 c, PART_COLUMN_CONTENT_TYPE); 402 if (contentType != null) { 403 part.setContentType(contentType); 404 } else { 405 throw new MmsException("Content-Type must be set."); 406 } 407 408 byte[] fileName = getByteArrayFromPartColumn( 409 c, PART_COLUMN_FILENAME); 410 if (fileName != null) { 411 part.setFilename(fileName); 412 } 413 414 byte[] name = getByteArrayFromPartColumn( 415 c, PART_COLUMN_NAME); 416 if (name != null) { 417 part.setName(name); 418 } 419 420 // Construct a Uri for this part. 421 long partId = c.getLong(PART_COLUMN_ID); 422 Uri partURI = Uri.parse("content://mms/part/" + partId); 423 part.setDataUri(partURI); 424 425 // For images/audio/video, we won't keep their data in Part 426 // because their renderer accept Uri as source. 427 String type = toIsoString(contentType); 428 if (!ContentType.isImageType(type) 429 && !ContentType.isAudioType(type) 430 && !ContentType.isVideoType(type)) { 431 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 432 InputStream is = null; 433 434 // Store simple string values directly in the database instead of an 435 // external file. This makes the text searchable and retrieval slightly 436 // faster. 437 if (ContentType.TEXT_PLAIN.equals(type) || ContentType.APP_SMIL.equals(type) 438 || ContentType.TEXT_HTML.equals(type)) { 439 String text = c.getString(PART_COLUMN_TEXT); 440 byte [] blob = new EncodedStringValue(text != null ? text : "") 441 .getTextString(); 442 baos.write(blob, 0, blob.length); 443 } else { 444 445 try { 446 is = mContentResolver.openInputStream(partURI); 447 448 byte[] buffer = new byte[256]; 449 int len = is.read(buffer); 450 while (len >= 0) { 451 baos.write(buffer, 0, len); 452 len = is.read(buffer); 453 } 454 } catch (IOException e) { 455 Log.e(TAG, "Failed to load part data", e); 456 c.close(); 457 throw new MmsException(e); 458 } finally { 459 if (is != null) { 460 try { 461 is.close(); 462 } catch (IOException e) { 463 Log.e(TAG, "Failed to close stream", e); 464 } // Ignore 465 } 466 } 467 } 468 part.setData(baos.toByteArray()); 469 } 470 parts[partIdx++] = part; 471 } 472 } finally { 473 if (c != null) { 474 c.close(); 475 } 476 } 477 478 return parts; 479 } 480 481 private void loadAddress(long msgId, PduHeaders headers) { 482 Cursor c = SqliteWrapper.query(mContext, mContentResolver, 483 Uri.parse("content://mms/" + msgId + "/addr"), 484 new String[] { Addr.ADDRESS, Addr.CHARSET, Addr.TYPE }, 485 null, null, null); 486 487 if (c != null) { 488 try { 489 while (c.moveToNext()) { 490 String addr = c.getString(0); 491 if (!TextUtils.isEmpty(addr)) { 492 int addrType = c.getInt(2); 493 switch (addrType) { 494 case PduHeaders.FROM: 495 headers.setEncodedStringValue( 496 new EncodedStringValue(c.getInt(1), getBytes(addr)), 497 addrType); 498 break; 499 case PduHeaders.TO: 500 case PduHeaders.CC: 501 case PduHeaders.BCC: 502 headers.appendEncodedStringValue( 503 new EncodedStringValue(c.getInt(1), getBytes(addr)), 504 addrType); 505 break; 506 default: 507 Log.e(TAG, "Unknown address type: " + addrType); 508 break; 509 } 510 } 511 } 512 } finally { 513 c.close(); 514 } 515 } 516 } 517 518 /** 519 * Load a PDU from storage by given Uri. 520 * 521 * @param uri The Uri of the PDU to be loaded. 522 * @return A generic PDU object, it may be cast to dedicated PDU. 523 * @throws MmsException Failed to load some fields of a PDU. 524 */ 525 public GenericPdu load(Uri uri) throws MmsException { 526 GenericPdu pdu = null; 527 PduCacheEntry cacheEntry = null; 528 int msgBox = 0; 529 long threadId = -1; 530 try { 531 synchronized(PDU_CACHE_INSTANCE) { 532 if (PDU_CACHE_INSTANCE.isUpdating(uri)) { 533 if (LOCAL_LOGV) { 534 Log.v(TAG, "load: " + uri + " blocked by isUpdating()"); 535 } 536 try { 537 PDU_CACHE_INSTANCE.wait(); 538 } catch (InterruptedException e) { 539 Log.e(TAG, "load: ", e); 540 } 541 cacheEntry = PDU_CACHE_INSTANCE.get(uri); 542 if (cacheEntry != null) { 543 return cacheEntry.getPdu(); 544 } 545 } 546 // Tell the cache to indicate to other callers that this item 547 // is currently being updated. 548 PDU_CACHE_INSTANCE.setUpdating(uri, true); 549 } 550 551 Cursor c = SqliteWrapper.query(mContext, mContentResolver, uri, 552 PDU_PROJECTION, null, null, null); 553 PduHeaders headers = new PduHeaders(); 554 Set<Entry<Integer, Integer>> set; 555 long msgId = ContentUris.parseId(uri); 556 557 try { 558 if ((c == null) || (c.getCount() != 1) || !c.moveToFirst()) { 559 throw new MmsException("Bad uri: " + uri); 560 } 561 562 msgBox = c.getInt(PDU_COLUMN_MESSAGE_BOX); 563 threadId = c.getLong(PDU_COLUMN_THREAD_ID); 564 565 set = ENCODED_STRING_COLUMN_INDEX_MAP.entrySet(); 566 for (Entry<Integer, Integer> e : set) { 567 setEncodedStringValueToHeaders( 568 c, e.getValue(), headers, e.getKey()); 569 } 570 571 set = TEXT_STRING_COLUMN_INDEX_MAP.entrySet(); 572 for (Entry<Integer, Integer> e : set) { 573 setTextStringToHeaders( 574 c, e.getValue(), headers, e.getKey()); 575 } 576 577 set = OCTET_COLUMN_INDEX_MAP.entrySet(); 578 for (Entry<Integer, Integer> e : set) { 579 setOctetToHeaders( 580 c, e.getValue(), headers, e.getKey()); 581 } 582 583 set = LONG_COLUMN_INDEX_MAP.entrySet(); 584 for (Entry<Integer, Integer> e : set) { 585 setLongToHeaders( 586 c, e.getValue(), headers, e.getKey()); 587 } 588 } finally { 589 if (c != null) { 590 c.close(); 591 } 592 } 593 594 // Check whether 'msgId' has been assigned a valid value. 595 if (msgId == -1L) { 596 throw new MmsException("Error! ID of the message: -1."); 597 } 598 599 // Load address information of the MM. 600 loadAddress(msgId, headers); 601 602 int msgType = headers.getOctet(PduHeaders.MESSAGE_TYPE); 603 PduBody body = new PduBody(); 604 605 // For PDU which type is M_retrieve.conf or Send.req, we should 606 // load multiparts and put them into the body of the PDU. 607 if ((msgType == PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF) 608 || (msgType == PduHeaders.MESSAGE_TYPE_SEND_REQ)) { 609 PduPart[] parts = loadParts(msgId); 610 if (parts != null) { 611 int partsNum = parts.length; 612 for (int i = 0; i < partsNum; i++) { 613 body.addPart(parts[i]); 614 } 615 } 616 } 617 618 switch (msgType) { 619 case PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND: 620 pdu = new NotificationInd(headers); 621 break; 622 case PduHeaders.MESSAGE_TYPE_DELIVERY_IND: 623 pdu = new DeliveryInd(headers); 624 break; 625 case PduHeaders.MESSAGE_TYPE_READ_ORIG_IND: 626 pdu = new ReadOrigInd(headers); 627 break; 628 case PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF: 629 pdu = new RetrieveConf(headers, body); 630 break; 631 case PduHeaders.MESSAGE_TYPE_SEND_REQ: 632 pdu = new SendReq(headers, body); 633 break; 634 case PduHeaders.MESSAGE_TYPE_ACKNOWLEDGE_IND: 635 pdu = new AcknowledgeInd(headers); 636 break; 637 case PduHeaders.MESSAGE_TYPE_NOTIFYRESP_IND: 638 pdu = new NotifyRespInd(headers); 639 break; 640 case PduHeaders.MESSAGE_TYPE_READ_REC_IND: 641 pdu = new ReadRecInd(headers); 642 break; 643 case PduHeaders.MESSAGE_TYPE_SEND_CONF: 644 case PduHeaders.MESSAGE_TYPE_FORWARD_REQ: 645 case PduHeaders.MESSAGE_TYPE_FORWARD_CONF: 646 case PduHeaders.MESSAGE_TYPE_MBOX_STORE_REQ: 647 case PduHeaders.MESSAGE_TYPE_MBOX_STORE_CONF: 648 case PduHeaders.MESSAGE_TYPE_MBOX_VIEW_REQ: 649 case PduHeaders.MESSAGE_TYPE_MBOX_VIEW_CONF: 650 case PduHeaders.MESSAGE_TYPE_MBOX_UPLOAD_REQ: 651 case PduHeaders.MESSAGE_TYPE_MBOX_UPLOAD_CONF: 652 case PduHeaders.MESSAGE_TYPE_MBOX_DELETE_REQ: 653 case PduHeaders.MESSAGE_TYPE_MBOX_DELETE_CONF: 654 case PduHeaders.MESSAGE_TYPE_MBOX_DESCR: 655 case PduHeaders.MESSAGE_TYPE_DELETE_REQ: 656 case PduHeaders.MESSAGE_TYPE_DELETE_CONF: 657 case PduHeaders.MESSAGE_TYPE_CANCEL_REQ: 658 case PduHeaders.MESSAGE_TYPE_CANCEL_CONF: 659 throw new MmsException( 660 "Unsupported PDU type: " + Integer.toHexString(msgType)); 661 662 default: 663 throw new MmsException( 664 "Unrecognized PDU type: " + Integer.toHexString(msgType)); 665 } 666 } finally { 667 synchronized(PDU_CACHE_INSTANCE) { 668 if (pdu != null) { 669 assert(PDU_CACHE_INSTANCE.get(uri) == null); 670 // Update the cache entry with the real info 671 cacheEntry = new PduCacheEntry(pdu, msgBox, threadId); 672 PDU_CACHE_INSTANCE.put(uri, cacheEntry); 673 } 674 PDU_CACHE_INSTANCE.setUpdating(uri, false); 675 PDU_CACHE_INSTANCE.notifyAll(); // tell anybody waiting on this entry to go ahead 676 } 677 } 678 return pdu; 679 } 680 681 private void persistAddress( 682 long msgId, int type, EncodedStringValue[] array) { 683 ContentValues values = new ContentValues(3); 684 685 for (EncodedStringValue addr : array) { 686 values.clear(); // Clear all values first. 687 values.put(Addr.ADDRESS, toIsoString(addr.getTextString())); 688 values.put(Addr.CHARSET, addr.getCharacterSet()); 689 values.put(Addr.TYPE, type); 690 691 Uri uri = Uri.parse("content://mms/" + msgId + "/addr"); 692 SqliteWrapper.insert(mContext, mContentResolver, uri, values); 693 } 694 } 695 696 private static String getPartContentType(PduPart part) { 697 return part.getContentType() == null ? null : toIsoString(part.getContentType()); 698 } 699 700 public Uri persistPart(PduPart part, long msgId, HashMap<Uri, InputStream> preOpenedFiles) 701 throws MmsException { 702 Uri uri = Uri.parse("content://mms/" + msgId + "/part"); 703 ContentValues values = new ContentValues(8); 704 705 int charset = part.getCharset(); 706 if (charset != 0 ) { 707 values.put(Part.CHARSET, charset); 708 } 709 710 String contentType = getPartContentType(part); 711 if (contentType != null) { 712 // There is no "image/jpg" in Android (and it's an invalid mimetype). 713 // Change it to "image/jpeg" 714 if (ContentType.IMAGE_JPG.equals(contentType)) { 715 contentType = ContentType.IMAGE_JPEG; 716 } 717 718 values.put(Part.CONTENT_TYPE, contentType); 719 // To ensure the SMIL part is always the first part. 720 if (ContentType.APP_SMIL.equals(contentType)) { 721 values.put(Part.SEQ, -1); 722 } 723 } else { 724 throw new MmsException("MIME type of the part must be set."); 725 } 726 727 if (part.getFilename() != null) { 728 String fileName = new String(part.getFilename()); 729 values.put(Part.FILENAME, fileName); 730 } 731 732 if (part.getName() != null) { 733 String name = new String(part.getName()); 734 values.put(Part.NAME, name); 735 } 736 737 Object value = null; 738 if (part.getContentDisposition() != null) { 739 value = toIsoString(part.getContentDisposition()); 740 values.put(Part.CONTENT_DISPOSITION, (String) value); 741 } 742 743 if (part.getContentId() != null) { 744 value = toIsoString(part.getContentId()); 745 values.put(Part.CONTENT_ID, (String) value); 746 } 747 748 if (part.getContentLocation() != null) { 749 value = toIsoString(part.getContentLocation()); 750 values.put(Part.CONTENT_LOCATION, (String) value); 751 } 752 753 Uri res = SqliteWrapper.insert(mContext, mContentResolver, uri, values); 754 if (res == null) { 755 throw new MmsException("Failed to persist part, return null."); 756 } 757 758 persistData(part, res, contentType, preOpenedFiles); 759 // After successfully store the data, we should update 760 // the dataUri of the part. 761 part.setDataUri(res); 762 763 return res; 764 } 765 766 /** 767 * Save data of the part into storage. The source data may be given 768 * by a byte[] or a Uri. If it's a byte[], directly save it 769 * into storage, otherwise load source data from the dataUri and then 770 * save it. If the data is an image, we may scale down it according 771 * to user preference. 772 * 773 * @param part The PDU part which contains data to be saved. 774 * @param uri The URI of the part. 775 * @param contentType The MIME type of the part. 776 * @param preOpenedFiles if not null, a map of preopened InputStreams for the parts. 777 * @throws MmsException Cannot find source data or error occurred 778 * while saving the data. 779 */ 780 private void persistData(PduPart part, Uri uri, 781 String contentType, HashMap<Uri, InputStream> preOpenedFiles) 782 throws MmsException { 783 OutputStream os = null; 784 InputStream is = null; 785 DrmConvertSession drmConvertSession = null; 786 Uri dataUri = null; 787 String path = null; 788 789 try { 790 byte[] data = part.getData(); 791 if (ContentType.TEXT_PLAIN.equals(contentType) 792 || ContentType.APP_SMIL.equals(contentType) 793 || ContentType.TEXT_HTML.equals(contentType)) { 794 ContentValues cv = new ContentValues(); 795 cv.put(Telephony.Mms.Part.TEXT, new EncodedStringValue(data).getString()); 796 if (mContentResolver.update(uri, cv, null, null) != 1) { 797 throw new MmsException("unable to update " + uri.toString()); 798 } 799 } else { 800 boolean isDrm = DownloadDrmHelper.isDrmConvertNeeded(contentType); 801 if (isDrm) { 802 if (uri != null) { 803 try { 804 path = convertUriToPath(mContext, uri); 805 if (LOCAL_LOGV) { 806 Log.v(TAG, "drm uri: " + uri + " path: " + path); 807 } 808 File f = new File(path); 809 long len = f.length(); 810 if (LOCAL_LOGV) { 811 Log.v(TAG, "drm path: " + path + " len: " + len); 812 } 813 if (len > 0) { 814 // we're not going to re-persist and re-encrypt an already 815 // converted drm file 816 return; 817 } 818 } catch (Exception e) { 819 Log.e(TAG, "Can't get file info for: " + part.getDataUri(), e); 820 } 821 } 822 // We haven't converted the file yet, start the conversion 823 drmConvertSession = DrmConvertSession.open(mContext, contentType); 824 if (drmConvertSession == null) { 825 throw new MmsException("Mimetype " + contentType + 826 " can not be converted."); 827 } 828 } 829 // uri can look like: 830 // content://mms/part/98 831 os = mContentResolver.openOutputStream(uri); 832 if (data == null) { 833 dataUri = part.getDataUri(); 834 if ((dataUri == null) || (dataUri == uri)) { 835 Log.w(TAG, "Can't find data for this part."); 836 return; 837 } 838 // dataUri can look like: 839 // content://com.google.android.gallery3d.provider/picasa/item/5720646660183715586 840 if (preOpenedFiles != null && preOpenedFiles.containsKey(dataUri)) { 841 is = preOpenedFiles.get(dataUri); 842 } 843 if (is == null) { 844 is = mContentResolver.openInputStream(dataUri); 845 } 846 847 if (LOCAL_LOGV) { 848 Log.v(TAG, "Saving data to: " + uri); 849 } 850 851 byte[] buffer = new byte[8192]; 852 for (int len = 0; (len = is.read(buffer)) != -1; ) { 853 if (!isDrm) { 854 os.write(buffer, 0, len); 855 } else { 856 byte[] convertedData = drmConvertSession.convert(buffer, len); 857 if (convertedData != null) { 858 os.write(convertedData, 0, convertedData.length); 859 } else { 860 throw new MmsException("Error converting drm data."); 861 } 862 } 863 } 864 } else { 865 if (LOCAL_LOGV) { 866 Log.v(TAG, "Saving data to: " + uri); 867 } 868 if (!isDrm) { 869 os.write(data); 870 } else { 871 dataUri = uri; 872 byte[] convertedData = drmConvertSession.convert(data, data.length); 873 if (convertedData != null) { 874 os.write(convertedData, 0, convertedData.length); 875 } else { 876 throw new MmsException("Error converting drm data."); 877 } 878 } 879 } 880 } 881 } catch (FileNotFoundException e) { 882 Log.e(TAG, "Failed to open Input/Output stream.", e); 883 throw new MmsException(e); 884 } catch (IOException e) { 885 Log.e(TAG, "Failed to read/write data.", e); 886 throw new MmsException(e); 887 } finally { 888 if (os != null) { 889 try { 890 os.close(); 891 } catch (IOException e) { 892 Log.e(TAG, "IOException while closing: " + os, e); 893 } // Ignore 894 } 895 if (is != null) { 896 try { 897 is.close(); 898 } catch (IOException e) { 899 Log.e(TAG, "IOException while closing: " + is, e); 900 } // Ignore 901 } 902 if (drmConvertSession != null) { 903 drmConvertSession.close(path); 904 905 // Reset the permissions on the encrypted part file so everyone has only read 906 // permission. 907 File f = new File(path); 908 ContentValues values = new ContentValues(0); 909 SqliteWrapper.update(mContext, mContentResolver, 910 Uri.parse("content://mms/resetFilePerm/" + f.getName()), 911 values, null, null); 912 } 913 } 914 } 915 916 /** 917 * This method expects uri in the following format 918 * content://media/<table_name>/<row_index> (or) 919 * file://sdcard/test.mp4 920 * http://test.com/test.mp4 921 * 922 * Here <table_name> shall be "video" or "audio" or "images" 923 * <row_index> the index of the content in given table 924 */ 925 static public String convertUriToPath(Context context, Uri uri) { 926 String path = null; 927 if (null != uri) { 928 String scheme = uri.getScheme(); 929 if (null == scheme || scheme.equals("") || 930 scheme.equals(ContentResolver.SCHEME_FILE)) { 931 path = uri.getPath(); 932 933 } else if (scheme.equals("http")) { 934 path = uri.toString(); 935 936 } else if (scheme.equals(ContentResolver.SCHEME_CONTENT)) { 937 String[] projection = new String[] {MediaStore.MediaColumns.DATA}; 938 Cursor cursor = null; 939 try { 940 cursor = context.getContentResolver().query(uri, projection, null, 941 null, null); 942 if (null == cursor || 0 == cursor.getCount() || !cursor.moveToFirst()) { 943 throw new IllegalArgumentException("Given Uri could not be found" + 944 " in media store"); 945 } 946 int pathIndex = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA); 947 path = cursor.getString(pathIndex); 948 } catch (SQLiteException e) { 949 throw new IllegalArgumentException("Given Uri is not formatted in a way " + 950 "so that it can be found in media store."); 951 } finally { 952 if (null != cursor) { 953 cursor.close(); 954 } 955 } 956 } else { 957 throw new IllegalArgumentException("Given Uri scheme is not supported"); 958 } 959 } 960 return path; 961 } 962 963 private void updateAddress( 964 long msgId, int type, EncodedStringValue[] array) { 965 // Delete old address information and then insert new ones. 966 SqliteWrapper.delete(mContext, mContentResolver, 967 Uri.parse("content://mms/" + msgId + "/addr"), 968 Addr.TYPE + "=" + type, null); 969 970 persistAddress(msgId, type, array); 971 } 972 973 /** 974 * Update headers of a SendReq. 975 * 976 * @param uri The PDU which need to be updated. 977 * @param pdu New headers. 978 * @throws MmsException Bad URI or updating failed. 979 */ 980 public void updateHeaders(Uri uri, SendReq sendReq) { 981 synchronized(PDU_CACHE_INSTANCE) { 982 // If the cache item is getting updated, wait until it's done updating before 983 // purging it. 984 if (PDU_CACHE_INSTANCE.isUpdating(uri)) { 985 if (LOCAL_LOGV) { 986 Log.v(TAG, "updateHeaders: " + uri + " blocked by isUpdating()"); 987 } 988 try { 989 PDU_CACHE_INSTANCE.wait(); 990 } catch (InterruptedException e) { 991 Log.e(TAG, "updateHeaders: ", e); 992 } 993 } 994 } 995 PDU_CACHE_INSTANCE.purge(uri); 996 997 ContentValues values = new ContentValues(10); 998 byte[] contentType = sendReq.getContentType(); 999 if (contentType != null) { 1000 values.put(Mms.CONTENT_TYPE, toIsoString(contentType)); 1001 } 1002 1003 long date = sendReq.getDate(); 1004 if (date != -1) { 1005 values.put(Mms.DATE, date); 1006 } 1007 1008 int deliveryReport = sendReq.getDeliveryReport(); 1009 if (deliveryReport != 0) { 1010 values.put(Mms.DELIVERY_REPORT, deliveryReport); 1011 } 1012 1013 long expiry = sendReq.getExpiry(); 1014 if (expiry != -1) { 1015 values.put(Mms.EXPIRY, expiry); 1016 } 1017 1018 byte[] msgClass = sendReq.getMessageClass(); 1019 if (msgClass != null) { 1020 values.put(Mms.MESSAGE_CLASS, toIsoString(msgClass)); 1021 } 1022 1023 int priority = sendReq.getPriority(); 1024 if (priority != 0) { 1025 values.put(Mms.PRIORITY, priority); 1026 } 1027 1028 int readReport = sendReq.getReadReport(); 1029 if (readReport != 0) { 1030 values.put(Mms.READ_REPORT, readReport); 1031 } 1032 1033 byte[] transId = sendReq.getTransactionId(); 1034 if (transId != null) { 1035 values.put(Mms.TRANSACTION_ID, toIsoString(transId)); 1036 } 1037 1038 EncodedStringValue subject = sendReq.getSubject(); 1039 if (subject != null) { 1040 values.put(Mms.SUBJECT, toIsoString(subject.getTextString())); 1041 values.put(Mms.SUBJECT_CHARSET, subject.getCharacterSet()); 1042 } else { 1043 values.put(Mms.SUBJECT, ""); 1044 } 1045 1046 long messageSize = sendReq.getMessageSize(); 1047 if (messageSize > 0) { 1048 values.put(Mms.MESSAGE_SIZE, messageSize); 1049 } 1050 1051 PduHeaders headers = sendReq.getPduHeaders(); 1052 HashSet<String> recipients = new HashSet<String>(); 1053 for (int addrType : ADDRESS_FIELDS) { 1054 EncodedStringValue[] array = null; 1055 if (addrType == PduHeaders.FROM) { 1056 EncodedStringValue v = headers.getEncodedStringValue(addrType); 1057 if (v != null) { 1058 array = new EncodedStringValue[1]; 1059 array[0] = v; 1060 } 1061 } else { 1062 array = headers.getEncodedStringValues(addrType); 1063 } 1064 1065 if (array != null) { 1066 long msgId = ContentUris.parseId(uri); 1067 updateAddress(msgId, addrType, array); 1068 if (addrType == PduHeaders.TO) { 1069 for (EncodedStringValue v : array) { 1070 if (v != null) { 1071 recipients.add(v.getString()); 1072 } 1073 } 1074 } 1075 } 1076 } 1077 if (!recipients.isEmpty()) { 1078 long threadId = Threads.getOrCreateThreadId(mContext, recipients); 1079 values.put(Mms.THREAD_ID, threadId); 1080 } 1081 1082 SqliteWrapper.update(mContext, mContentResolver, uri, values, null, null); 1083 } 1084 1085 private void updatePart(Uri uri, PduPart part, HashMap<Uri, InputStream> preOpenedFiles) 1086 throws MmsException { 1087 ContentValues values = new ContentValues(7); 1088 1089 int charset = part.getCharset(); 1090 if (charset != 0 ) { 1091 values.put(Part.CHARSET, charset); 1092 } 1093 1094 String contentType = null; 1095 if (part.getContentType() != null) { 1096 contentType = toIsoString(part.getContentType()); 1097 values.put(Part.CONTENT_TYPE, contentType); 1098 } else { 1099 throw new MmsException("MIME type of the part must be set."); 1100 } 1101 1102 if (part.getFilename() != null) { 1103 String fileName = new String(part.getFilename()); 1104 values.put(Part.FILENAME, fileName); 1105 } 1106 1107 if (part.getName() != null) { 1108 String name = new String(part.getName()); 1109 values.put(Part.NAME, name); 1110 } 1111 1112 Object value = null; 1113 if (part.getContentDisposition() != null) { 1114 value = toIsoString(part.getContentDisposition()); 1115 values.put(Part.CONTENT_DISPOSITION, (String) value); 1116 } 1117 1118 if (part.getContentId() != null) { 1119 value = toIsoString(part.getContentId()); 1120 values.put(Part.CONTENT_ID, (String) value); 1121 } 1122 1123 if (part.getContentLocation() != null) { 1124 value = toIsoString(part.getContentLocation()); 1125 values.put(Part.CONTENT_LOCATION, (String) value); 1126 } 1127 1128 SqliteWrapper.update(mContext, mContentResolver, uri, values, null, null); 1129 1130 // Only update the data when: 1131 // 1. New binary data supplied or 1132 // 2. The Uri of the part is different from the current one. 1133 if ((part.getData() != null) 1134 || (uri != part.getDataUri())) { 1135 persistData(part, uri, contentType, preOpenedFiles); 1136 } 1137 } 1138 1139 /** 1140 * Update all parts of a PDU. 1141 * 1142 * @param uri The PDU which need to be updated. 1143 * @param body New message body of the PDU. 1144 * @param preOpenedFiles if not null, a map of preopened InputStreams for the parts. 1145 * @throws MmsException Bad URI or updating failed. 1146 */ 1147 public void updateParts(Uri uri, PduBody body, HashMap<Uri, InputStream> preOpenedFiles) 1148 throws MmsException { 1149 try { 1150 PduCacheEntry cacheEntry; 1151 synchronized(PDU_CACHE_INSTANCE) { 1152 if (PDU_CACHE_INSTANCE.isUpdating(uri)) { 1153 if (LOCAL_LOGV) { 1154 Log.v(TAG, "updateParts: " + uri + " blocked by isUpdating()"); 1155 } 1156 try { 1157 PDU_CACHE_INSTANCE.wait(); 1158 } catch (InterruptedException e) { 1159 Log.e(TAG, "updateParts: ", e); 1160 } 1161 cacheEntry = PDU_CACHE_INSTANCE.get(uri); 1162 if (cacheEntry != null) { 1163 ((MultimediaMessagePdu) cacheEntry.getPdu()).setBody(body); 1164 } 1165 } 1166 // Tell the cache to indicate to other callers that this item 1167 // is currently being updated. 1168 PDU_CACHE_INSTANCE.setUpdating(uri, true); 1169 } 1170 1171 ArrayList<PduPart> toBeCreated = new ArrayList<PduPart>(); 1172 HashMap<Uri, PduPart> toBeUpdated = new HashMap<Uri, PduPart>(); 1173 1174 int partsNum = body.getPartsNum(); 1175 StringBuilder filter = new StringBuilder().append('('); 1176 for (int i = 0; i < partsNum; i++) { 1177 PduPart part = body.getPart(i); 1178 Uri partUri = part.getDataUri(); 1179 if ((partUri == null) || !partUri.getAuthority().startsWith("mms")) { 1180 toBeCreated.add(part); 1181 } else { 1182 toBeUpdated.put(partUri, part); 1183 1184 // Don't use 'i > 0' to determine whether we should append 1185 // 'AND' since 'i = 0' may be skipped in another branch. 1186 if (filter.length() > 1) { 1187 filter.append(" AND "); 1188 } 1189 1190 filter.append(Part._ID); 1191 filter.append("!="); 1192 DatabaseUtils.appendEscapedSQLString(filter, partUri.getLastPathSegment()); 1193 } 1194 } 1195 filter.append(')'); 1196 1197 long msgId = ContentUris.parseId(uri); 1198 1199 // Remove the parts which doesn't exist anymore. 1200 SqliteWrapper.delete(mContext, mContentResolver, 1201 Uri.parse(Mms.CONTENT_URI + "/" + msgId + "/part"), 1202 filter.length() > 2 ? filter.toString() : null, null); 1203 1204 // Create new parts which didn't exist before. 1205 for (PduPart part : toBeCreated) { 1206 persistPart(part, msgId, preOpenedFiles); 1207 } 1208 1209 // Update the modified parts. 1210 for (Map.Entry<Uri, PduPart> e : toBeUpdated.entrySet()) { 1211 updatePart(e.getKey(), e.getValue(), preOpenedFiles); 1212 } 1213 } finally { 1214 synchronized(PDU_CACHE_INSTANCE) { 1215 PDU_CACHE_INSTANCE.setUpdating(uri, false); 1216 PDU_CACHE_INSTANCE.notifyAll(); 1217 } 1218 } 1219 } 1220 1221 /** 1222 * Persist a PDU object to specific location in the storage. 1223 * 1224 * @param pdu The PDU object to be stored. 1225 * @param uri Where to store the given PDU object. 1226 * @param createThreadId if true, this function may create a thread id for the recipients 1227 * @param groupMmsEnabled if true, all of the recipients addressed in the PDU will be used 1228 * to create the associated thread. When false, only the sender will be used in finding or 1229 * creating the appropriate thread or conversation. 1230 * @param preOpenedFiles if not null, a map of preopened InputStreams for the parts. 1231 * @return A Uri which can be used to access the stored PDU. 1232 */ 1233 1234 public Uri persist(GenericPdu pdu, Uri uri, boolean createThreadId, boolean groupMmsEnabled, 1235 HashMap<Uri, InputStream> preOpenedFiles) 1236 throws MmsException { 1237 if (uri == null) { 1238 throw new MmsException("Uri may not be null."); 1239 } 1240 long msgId = -1; 1241 try { 1242 msgId = ContentUris.parseId(uri); 1243 } catch (NumberFormatException e) { 1244 // the uri ends with "inbox" or something else like that 1245 } 1246 boolean existingUri = msgId != -1; 1247 1248 if (!existingUri && MESSAGE_BOX_MAP.get(uri) == null) { 1249 throw new MmsException( 1250 "Bad destination, must be one of " 1251 + "content://mms/inbox, content://mms/sent, " 1252 + "content://mms/drafts, content://mms/outbox, " 1253 + "content://mms/temp."); 1254 } 1255 synchronized(PDU_CACHE_INSTANCE) { 1256 // If the cache item is getting updated, wait until it's done updating before 1257 // purging it. 1258 if (PDU_CACHE_INSTANCE.isUpdating(uri)) { 1259 if (LOCAL_LOGV) { 1260 Log.v(TAG, "persist: " + uri + " blocked by isUpdating()"); 1261 } 1262 try { 1263 PDU_CACHE_INSTANCE.wait(); 1264 } catch (InterruptedException e) { 1265 Log.e(TAG, "persist1: ", e); 1266 } 1267 } 1268 } 1269 PDU_CACHE_INSTANCE.purge(uri); 1270 1271 PduHeaders header = pdu.getPduHeaders(); 1272 PduBody body = null; 1273 ContentValues values = new ContentValues(); 1274 Set<Entry<Integer, String>> set; 1275 1276 set = ENCODED_STRING_COLUMN_NAME_MAP.entrySet(); 1277 for (Entry<Integer, String> e : set) { 1278 int field = e.getKey(); 1279 EncodedStringValue encodedString = header.getEncodedStringValue(field); 1280 if (encodedString != null) { 1281 String charsetColumn = CHARSET_COLUMN_NAME_MAP.get(field); 1282 values.put(e.getValue(), toIsoString(encodedString.getTextString())); 1283 values.put(charsetColumn, encodedString.getCharacterSet()); 1284 } 1285 } 1286 1287 set = TEXT_STRING_COLUMN_NAME_MAP.entrySet(); 1288 for (Entry<Integer, String> e : set){ 1289 byte[] text = header.getTextString(e.getKey()); 1290 if (text != null) { 1291 values.put(e.getValue(), toIsoString(text)); 1292 } 1293 } 1294 1295 set = OCTET_COLUMN_NAME_MAP.entrySet(); 1296 for (Entry<Integer, String> e : set){ 1297 int b = header.getOctet(e.getKey()); 1298 if (b != 0) { 1299 values.put(e.getValue(), b); 1300 } 1301 } 1302 1303 set = LONG_COLUMN_NAME_MAP.entrySet(); 1304 for (Entry<Integer, String> e : set){ 1305 long l = header.getLongInteger(e.getKey()); 1306 if (l != -1L) { 1307 values.put(e.getValue(), l); 1308 } 1309 } 1310 1311 HashMap<Integer, EncodedStringValue[]> addressMap = 1312 new HashMap<Integer, EncodedStringValue[]>(ADDRESS_FIELDS.length); 1313 // Save address information. 1314 for (int addrType : ADDRESS_FIELDS) { 1315 EncodedStringValue[] array = null; 1316 if (addrType == PduHeaders.FROM) { 1317 EncodedStringValue v = header.getEncodedStringValue(addrType); 1318 if (v != null) { 1319 array = new EncodedStringValue[1]; 1320 array[0] = v; 1321 } 1322 } else { 1323 array = header.getEncodedStringValues(addrType); 1324 } 1325 addressMap.put(addrType, array); 1326 } 1327 1328 HashSet<String> recipients = new HashSet<String>(); 1329 int msgType = pdu.getMessageType(); 1330 // Here we only allocate thread ID for M-Notification.ind, 1331 // M-Retrieve.conf and M-Send.req. 1332 // Some of other PDU types may be allocated a thread ID outside 1333 // this scope. 1334 if ((msgType == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) 1335 || (msgType == PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF) 1336 || (msgType == PduHeaders.MESSAGE_TYPE_SEND_REQ)) { 1337 switch (msgType) { 1338 case PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND: 1339 case PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF: 1340 loadRecipients(PduHeaders.FROM, recipients, addressMap, false); 1341 1342 // For received messages when group MMS is enabled, we want to associate this 1343 // message with the thread composed of all the recipients -- all but our own 1344 // number, that is. This includes the person who sent the 1345 // message or the FROM field (above) in addition to the other people the message 1346 // was addressed to or the TO field. Our own number is in that TO field and 1347 // we have to ignore it in loadRecipients. 1348 if (groupMmsEnabled) { 1349 loadRecipients(PduHeaders.TO, recipients, addressMap, true); 1350 } 1351 break; 1352 case PduHeaders.MESSAGE_TYPE_SEND_REQ: 1353 loadRecipients(PduHeaders.TO, recipients, addressMap, false); 1354 break; 1355 } 1356 long threadId = 0; 1357 if (createThreadId && !recipients.isEmpty()) { 1358 // Given all the recipients associated with this message, find (or create) the 1359 // correct thread. 1360 threadId = Threads.getOrCreateThreadId(mContext, recipients); 1361 } 1362 values.put(Mms.THREAD_ID, threadId); 1363 } 1364 1365 // Save parts first to avoid inconsistent message is loaded 1366 // while saving the parts. 1367 long dummyId = System.currentTimeMillis(); // Dummy ID of the msg. 1368 1369 // Figure out if this PDU is a text-only message 1370 boolean textOnly = true; 1371 1372 // Get body if the PDU is a RetrieveConf or SendReq. 1373 if (pdu instanceof MultimediaMessagePdu) { 1374 body = ((MultimediaMessagePdu) pdu).getBody(); 1375 // Start saving parts if necessary. 1376 if (body != null) { 1377 int partsNum = body.getPartsNum(); 1378 if (partsNum > 2) { 1379 // For a text-only message there will be two parts: 1-the SMIL, 2-the text. 1380 // Down a few lines below we're checking to make sure we've only got SMIL or 1381 // text. We also have to check then we don't have more than two parts. 1382 // Otherwise, a slideshow with two text slides would be marked as textOnly. 1383 textOnly = false; 1384 } 1385 for (int i = 0; i < partsNum; i++) { 1386 PduPart part = body.getPart(i); 1387 persistPart(part, dummyId, preOpenedFiles); 1388 1389 // If we've got anything besides text/plain or SMIL part, then we've got 1390 // an mms message with some other type of attachment. 1391 String contentType = getPartContentType(part); 1392 if (contentType != null && !ContentType.APP_SMIL.equals(contentType) 1393 && !ContentType.TEXT_PLAIN.equals(contentType)) { 1394 textOnly = false; 1395 } 1396 } 1397 } 1398 } 1399 // Record whether this mms message is a simple plain text or not. This is a hint for the 1400 // UI. 1401 values.put(Mms.TEXT_ONLY, textOnly ? 1 : 0); 1402 1403 Uri res = null; 1404 if (existingUri) { 1405 res = uri; 1406 SqliteWrapper.update(mContext, mContentResolver, res, values, null, null); 1407 } else { 1408 res = SqliteWrapper.insert(mContext, mContentResolver, uri, values); 1409 if (res == null) { 1410 throw new MmsException("persist() failed: return null."); 1411 } 1412 // Get the real ID of the PDU and update all parts which were 1413 // saved with the dummy ID. 1414 msgId = ContentUris.parseId(res); 1415 } 1416 1417 values = new ContentValues(1); 1418 values.put(Part.MSG_ID, msgId); 1419 SqliteWrapper.update(mContext, mContentResolver, 1420 Uri.parse("content://mms/" + dummyId + "/part"), 1421 values, null, null); 1422 // We should return the longest URI of the persisted PDU, for 1423 // example, if input URI is "content://mms/inbox" and the _ID of 1424 // persisted PDU is '8', we should return "content://mms/inbox/8" 1425 // instead of "content://mms/8". 1426 // FIXME: Should the MmsProvider be responsible for this??? 1427 if (!existingUri) { 1428 res = Uri.parse(uri + "/" + msgId); 1429 } 1430 1431 // Save address information. 1432 for (int addrType : ADDRESS_FIELDS) { 1433 EncodedStringValue[] array = addressMap.get(addrType); 1434 if (array != null) { 1435 persistAddress(msgId, addrType, array); 1436 } 1437 } 1438 1439 return res; 1440 } 1441 1442 /** 1443 * For a given address type, extract the recipients from the headers. 1444 * 1445 * @param addressType can be PduHeaders.FROM or PduHeaders.TO 1446 * @param recipients a HashSet that is loaded with the recipients from the FROM or TO headers 1447 * @param addressMap a HashMap of the addresses from the ADDRESS_FIELDS header 1448 * @param excludeMyNumber if true, the number of this phone will be excluded from recipients 1449 */ 1450 private void loadRecipients(int addressType, HashSet<String> recipients, 1451 HashMap<Integer, EncodedStringValue[]> addressMap, boolean excludeMyNumber) { 1452 EncodedStringValue[] array = addressMap.get(addressType); 1453 if (array == null) { 1454 return; 1455 } 1456 // If the TO recipients is only a single address, then we can skip loadRecipients when 1457 // we're excluding our own number because we know that address is our own. 1458 if (excludeMyNumber && array.length == 1) { 1459 return; 1460 } 1461 String myNumber = excludeMyNumber ? mTelephonyManager.getLine1Number() : null; 1462 for (EncodedStringValue v : array) { 1463 if (v != null) { 1464 String number = v.getString(); 1465 if ((myNumber == null || !PhoneNumberUtils.compare(number, myNumber)) && 1466 !recipients.contains(number)) { 1467 // Only add numbers which aren't my own number. 1468 recipients.add(number); 1469 } 1470 } 1471 } 1472 } 1473 1474 /** 1475 * Move a PDU object from one location to another. 1476 * 1477 * @param from Specify the PDU object to be moved. 1478 * @param to The destination location, should be one of the following: 1479 * "content://mms/inbox", "content://mms/sent", 1480 * "content://mms/drafts", "content://mms/outbox", 1481 * "content://mms/trash". 1482 * @return New Uri of the moved PDU. 1483 * @throws MmsException Error occurred while moving the message. 1484 */ 1485 public Uri move(Uri from, Uri to) throws MmsException { 1486 // Check whether the 'msgId' has been assigned a valid value. 1487 long msgId = ContentUris.parseId(from); 1488 if (msgId == -1L) { 1489 throw new MmsException("Error! ID of the message: -1."); 1490 } 1491 1492 // Get corresponding int value of destination box. 1493 Integer msgBox = MESSAGE_BOX_MAP.get(to); 1494 if (msgBox == null) { 1495 throw new MmsException( 1496 "Bad destination, must be one of " 1497 + "content://mms/inbox, content://mms/sent, " 1498 + "content://mms/drafts, content://mms/outbox, " 1499 + "content://mms/temp."); 1500 } 1501 1502 ContentValues values = new ContentValues(1); 1503 values.put(Mms.MESSAGE_BOX, msgBox); 1504 SqliteWrapper.update(mContext, mContentResolver, from, values, null, null); 1505 return ContentUris.withAppendedId(to, msgId); 1506 } 1507 1508 /** 1509 * Wrap a byte[] into a String. 1510 */ 1511 public static String toIsoString(byte[] bytes) { 1512 try { 1513 return new String(bytes, CharacterSets.MIMENAME_ISO_8859_1); 1514 } catch (UnsupportedEncodingException e) { 1515 // Impossible to reach here! 1516 Log.e(TAG, "ISO_8859_1 must be supported!", e); 1517 return ""; 1518 } 1519 } 1520 1521 /** 1522 * Unpack a given String into a byte[]. 1523 */ 1524 public static byte[] getBytes(String data) { 1525 try { 1526 return data.getBytes(CharacterSets.MIMENAME_ISO_8859_1); 1527 } catch (UnsupportedEncodingException e) { 1528 // Impossible to reach here! 1529 Log.e(TAG, "ISO_8859_1 must be supported!", e); 1530 return new byte[0]; 1531 } 1532 } 1533 1534 /** 1535 * Remove all objects in the temporary path. 1536 */ 1537 public void release() { 1538 Uri uri = Uri.parse(TEMPORARY_DRM_OBJECT_URI); 1539 SqliteWrapper.delete(mContext, mContentResolver, uri, null, null); 1540 } 1541 1542 /** 1543 * Find all messages to be sent or downloaded before certain time. 1544 */ 1545 public Cursor getPendingMessages(long dueTime) { 1546 Uri.Builder uriBuilder = PendingMessages.CONTENT_URI.buildUpon(); 1547 uriBuilder.appendQueryParameter("protocol", "mms"); 1548 1549 String selection = PendingMessages.ERROR_TYPE + " < ?" 1550 + " AND " + PendingMessages.DUE_TIME + " <= ?"; 1551 1552 String[] selectionArgs = new String[] { 1553 String.valueOf(MmsSms.ERR_TYPE_GENERIC_PERMANENT), 1554 String.valueOf(dueTime) 1555 }; 1556 1557 return SqliteWrapper.query(mContext, mContentResolver, 1558 uriBuilder.build(), null, selection, selectionArgs, 1559 PendingMessages.DUE_TIME); 1560 } 1561 } 1562