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