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