1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.messaging.sms; 18 19 import android.content.ContentResolver; 20 import android.content.ContentUris; 21 import android.content.Context; 22 import android.content.res.AssetFileDescriptor; 23 import android.database.Cursor; 24 import android.graphics.Bitmap; 25 import android.graphics.BitmapFactory; 26 import android.media.MediaMetadataRetriever; 27 import android.net.Uri; 28 import android.os.Parcel; 29 import android.os.Parcelable; 30 import android.provider.Telephony.Mms; 31 import android.provider.Telephony.Sms; 32 import android.text.TextUtils; 33 import android.util.Log; 34 import android.webkit.MimeTypeMap; 35 36 import com.android.messaging.Factory; 37 import com.android.messaging.datamodel.data.MessageData; 38 import com.android.messaging.datamodel.media.VideoThumbnailRequest; 39 import com.android.messaging.mmslib.pdu.CharacterSets; 40 import com.android.messaging.util.Assert; 41 import com.android.messaging.util.ContentType; 42 import com.android.messaging.util.LogUtil; 43 import com.android.messaging.util.MediaMetadataRetrieverWrapper; 44 import com.android.messaging.util.OsUtil; 45 import com.android.messaging.util.PhoneUtils; 46 import com.google.common.collect.Lists; 47 48 import java.io.ByteArrayOutputStream; 49 import java.io.FileNotFoundException; 50 import java.io.IOException; 51 import java.io.InputStream; 52 import java.io.UnsupportedEncodingException; 53 import java.util.ArrayList; 54 import java.util.List; 55 56 /** 57 * Class contains various SMS/MMS database entities from telephony provider 58 */ 59 public class DatabaseMessages { 60 private static final String TAG = LogUtil.BUGLE_TAG; 61 62 public abstract static class DatabaseMessage { getProtocol()63 public abstract int getProtocol(); getUri()64 public abstract String getUri(); getTimestampInMillis()65 public abstract long getTimestampInMillis(); 66 67 @Override equals(final Object other)68 public boolean equals(final Object other) { 69 if (other == null || !(other instanceof DatabaseMessage)) { 70 return false; 71 } 72 final DatabaseMessage otherDbMsg = (DatabaseMessage) other; 73 // No need to check timestamp since we only need this when we compare 74 // messages at the same timestamp 75 return TextUtils.equals(getUri(), otherDbMsg.getUri()); 76 } 77 78 @Override hashCode()79 public int hashCode() { 80 // No need to check timestamp since we only need this when we compare 81 // messages at the same timestamp 82 return getUri().hashCode(); 83 } 84 } 85 86 /** 87 * SMS message 88 */ 89 public static class SmsMessage extends DatabaseMessage implements Parcelable { 90 private static int sIota = 0; 91 public static final int INDEX_ID = sIota++; 92 public static final int INDEX_TYPE = sIota++; 93 public static final int INDEX_ADDRESS = sIota++; 94 public static final int INDEX_BODY = sIota++; 95 public static final int INDEX_DATE = sIota++; 96 public static final int INDEX_THREAD_ID = sIota++; 97 public static final int INDEX_STATUS = sIota++; 98 public static final int INDEX_READ = sIota++; 99 public static final int INDEX_SEEN = sIota++; 100 public static final int INDEX_DATE_SENT = sIota++; 101 public static final int INDEX_SUB_ID = sIota++; 102 103 private static String[] sProjection; 104 getProjection()105 public static String[] getProjection() { 106 if (sProjection == null) { 107 String[] projection = new String[] { 108 Sms._ID, 109 Sms.TYPE, 110 Sms.ADDRESS, 111 Sms.BODY, 112 Sms.DATE, 113 Sms.THREAD_ID, 114 Sms.STATUS, 115 Sms.READ, 116 Sms.SEEN, 117 Sms.DATE_SENT, 118 Sms.SUBSCRIPTION_ID, 119 }; 120 if (!MmsUtils.hasSmsDateSentColumn()) { 121 projection[INDEX_DATE_SENT] = Sms.DATE; 122 } 123 if (!OsUtil.isAtLeastL_MR1()) { 124 Assert.equals(INDEX_SUB_ID, projection.length - 1); 125 String[] withoutSubId = new String[projection.length - 1]; 126 System.arraycopy(projection, 0, withoutSubId, 0, withoutSubId.length); 127 projection = withoutSubId; 128 } 129 130 sProjection = projection; 131 } 132 133 return sProjection; 134 } 135 136 public String mUri; 137 public String mAddress; 138 public String mBody; 139 private long mRowId; 140 public long mTimestampInMillis; 141 public long mTimestampSentInMillis; 142 public int mType; 143 public long mThreadId; 144 public int mStatus; 145 public boolean mRead; 146 public boolean mSeen; 147 public int mSubId; 148 SmsMessage()149 private SmsMessage() { 150 } 151 152 /** 153 * Load from a cursor of a query that returns the SMS to import 154 * 155 * @param cursor 156 */ load(final Cursor cursor)157 private void load(final Cursor cursor) { 158 mRowId = cursor.getLong(INDEX_ID); 159 mAddress = cursor.getString(INDEX_ADDRESS); 160 mBody = cursor.getString(INDEX_BODY); 161 mTimestampInMillis = cursor.getLong(INDEX_DATE); 162 // Before ICS, there is no "date_sent" so use copy of "date" value 163 mTimestampSentInMillis = cursor.getLong(INDEX_DATE_SENT); 164 mType = cursor.getInt(INDEX_TYPE); 165 mThreadId = cursor.getLong(INDEX_THREAD_ID); 166 mStatus = cursor.getInt(INDEX_STATUS); 167 mRead = cursor.getInt(INDEX_READ) == 0 ? false : true; 168 mSeen = cursor.getInt(INDEX_SEEN) == 0 ? false : true; 169 mUri = ContentUris.withAppendedId(Sms.CONTENT_URI, mRowId).toString(); 170 mSubId = PhoneUtils.getDefault().getSubIdFromTelephony(cursor, INDEX_SUB_ID); 171 } 172 173 /** 174 * Get a new SmsMessage by loading from the cursor of a query 175 * that returns the SMS to import 176 * 177 * @param cursor 178 * @return 179 */ get(final Cursor cursor)180 public static SmsMessage get(final Cursor cursor) { 181 final SmsMessage msg = new SmsMessage(); 182 msg.load(cursor); 183 return msg; 184 } 185 186 @Override getUri()187 public String getUri() { 188 return mUri; 189 } 190 getSubId()191 public int getSubId() { 192 return mSubId; 193 } 194 195 @Override getProtocol()196 public int getProtocol() { 197 return MessageData.PROTOCOL_SMS; 198 } 199 200 @Override getTimestampInMillis()201 public long getTimestampInMillis() { 202 return mTimestampInMillis; 203 } 204 205 @Override describeContents()206 public int describeContents() { 207 return 0; 208 } 209 SmsMessage(final Parcel in)210 private SmsMessage(final Parcel in) { 211 mUri = in.readString(); 212 mRowId = in.readLong(); 213 mTimestampInMillis = in.readLong(); 214 mTimestampSentInMillis = in.readLong(); 215 mType = in.readInt(); 216 mThreadId = in.readLong(); 217 mStatus = in.readInt(); 218 mRead = in.readInt() != 0; 219 mSeen = in.readInt() != 0; 220 mSubId = in.readInt(); 221 222 // SMS specific 223 mAddress = in.readString(); 224 mBody = in.readString(); 225 } 226 227 public static final Parcelable.Creator<SmsMessage> CREATOR 228 = new Parcelable.Creator<SmsMessage>() { 229 @Override 230 public SmsMessage createFromParcel(final Parcel in) { 231 return new SmsMessage(in); 232 } 233 234 @Override 235 public SmsMessage[] newArray(final int size) { 236 return new SmsMessage[size]; 237 } 238 }; 239 240 @Override writeToParcel(final Parcel out, final int flags)241 public void writeToParcel(final Parcel out, final int flags) { 242 out.writeString(mUri); 243 out.writeLong(mRowId); 244 out.writeLong(mTimestampInMillis); 245 out.writeLong(mTimestampSentInMillis); 246 out.writeInt(mType); 247 out.writeLong(mThreadId); 248 out.writeInt(mStatus); 249 out.writeInt(mRead ? 1 : 0); 250 out.writeInt(mSeen ? 1 : 0); 251 out.writeInt(mSubId); 252 253 // SMS specific 254 out.writeString(mAddress); 255 out.writeString(mBody); 256 } 257 } 258 259 /** 260 * MMS message 261 */ 262 public static class MmsMessage extends DatabaseMessage implements Parcelable { 263 private static int sIota = 0; 264 public static final int INDEX_ID = sIota++; 265 public static final int INDEX_MESSAGE_BOX = sIota++; 266 public static final int INDEX_SUBJECT = sIota++; 267 public static final int INDEX_SUBJECT_CHARSET = sIota++; 268 public static final int INDEX_MESSAGE_SIZE = sIota++; 269 public static final int INDEX_DATE = sIota++; 270 public static final int INDEX_DATE_SENT = sIota++; 271 public static final int INDEX_THREAD_ID = sIota++; 272 public static final int INDEX_PRIORITY = sIota++; 273 public static final int INDEX_STATUS = sIota++; 274 public static final int INDEX_READ = sIota++; 275 public static final int INDEX_SEEN = sIota++; 276 public static final int INDEX_CONTENT_LOCATION = sIota++; 277 public static final int INDEX_TRANSACTION_ID = sIota++; 278 public static final int INDEX_MESSAGE_TYPE = sIota++; 279 public static final int INDEX_EXPIRY = sIota++; 280 public static final int INDEX_RESPONSE_STATUS = sIota++; 281 public static final int INDEX_RETRIEVE_STATUS = sIota++; 282 public static final int INDEX_SUB_ID = sIota++; 283 284 private static String[] sProjection; 285 getProjection()286 public static String[] getProjection() { 287 if (sProjection == null) { 288 String[] projection = new String[] { 289 Mms._ID, 290 Mms.MESSAGE_BOX, 291 Mms.SUBJECT, 292 Mms.SUBJECT_CHARSET, 293 Mms.MESSAGE_SIZE, 294 Mms.DATE, 295 Mms.DATE_SENT, 296 Mms.THREAD_ID, 297 Mms.PRIORITY, 298 Mms.STATUS, 299 Mms.READ, 300 Mms.SEEN, 301 Mms.CONTENT_LOCATION, 302 Mms.TRANSACTION_ID, 303 Mms.MESSAGE_TYPE, 304 Mms.EXPIRY, 305 Mms.RESPONSE_STATUS, 306 Mms.RETRIEVE_STATUS, 307 Mms.SUBSCRIPTION_ID, 308 }; 309 310 if (!OsUtil.isAtLeastL_MR1()) { 311 Assert.equals(INDEX_SUB_ID, projection.length - 1); 312 String[] withoutSubId = new String[projection.length - 1]; 313 System.arraycopy(projection, 0, withoutSubId, 0, withoutSubId.length); 314 projection = withoutSubId; 315 } 316 317 sProjection = projection; 318 } 319 320 return sProjection; 321 } 322 323 public String mUri; 324 private long mRowId; 325 public int mType; 326 public String mSubject; 327 public int mSubjectCharset; 328 private long mSize; 329 public long mTimestampInMillis; 330 public long mSentTimestampInMillis; 331 public long mThreadId; 332 public int mPriority; 333 public int mStatus; 334 public boolean mRead; 335 public boolean mSeen; 336 public String mContentLocation; 337 public String mTransactionId; 338 public int mMmsMessageType; 339 public long mExpiryInMillis; 340 public int mSubId; 341 public String mSender; 342 public int mResponseStatus; 343 public int mRetrieveStatus; 344 345 public List<MmsPart> mParts = Lists.newArrayList(); 346 private boolean mPartsProcessed = false; 347 MmsMessage()348 private MmsMessage() { 349 } 350 351 /** 352 * Load from a cursor of a query that returns the MMS to import 353 * 354 * @param cursor 355 */ load(final Cursor cursor)356 public void load(final Cursor cursor) { 357 mRowId = cursor.getLong(INDEX_ID); 358 mType = cursor.getInt(INDEX_MESSAGE_BOX); 359 mSubject = cursor.getString(INDEX_SUBJECT); 360 mSubjectCharset = cursor.getInt(INDEX_SUBJECT_CHARSET); 361 if (!TextUtils.isEmpty(mSubject)) { 362 // PduPersister stores the subject using ISO_8859_1 363 // Let's load it using that encoding and convert it back to its original 364 // See PduPersister.persist and PduPersister.toIsoString 365 // (Refer to bug b/11162476) 366 mSubject = getDecodedString( 367 getStringBytes(mSubject, CharacterSets.ISO_8859_1), mSubjectCharset); 368 } 369 mSize = cursor.getLong(INDEX_MESSAGE_SIZE); 370 // MMS db times are in seconds 371 mTimestampInMillis = cursor.getLong(INDEX_DATE) * 1000; 372 mSentTimestampInMillis = cursor.getLong(INDEX_DATE_SENT) * 1000; 373 mThreadId = cursor.getLong(INDEX_THREAD_ID); 374 mPriority = cursor.getInt(INDEX_PRIORITY); 375 mStatus = cursor.getInt(INDEX_STATUS); 376 mRead = cursor.getInt(INDEX_READ) == 0 ? false : true; 377 mSeen = cursor.getInt(INDEX_SEEN) == 0 ? false : true; 378 mContentLocation = cursor.getString(INDEX_CONTENT_LOCATION); 379 mTransactionId = cursor.getString(INDEX_TRANSACTION_ID); 380 mMmsMessageType = cursor.getInt(INDEX_MESSAGE_TYPE); 381 mExpiryInMillis = cursor.getLong(INDEX_EXPIRY) * 1000; 382 mResponseStatus = cursor.getInt(INDEX_RESPONSE_STATUS); 383 mRetrieveStatus = cursor.getInt(INDEX_RETRIEVE_STATUS); 384 // Clear all parts in case we reuse this object 385 mParts.clear(); 386 mPartsProcessed = false; 387 mUri = ContentUris.withAppendedId(Mms.CONTENT_URI, mRowId).toString(); 388 mSubId = PhoneUtils.getDefault().getSubIdFromTelephony(cursor, INDEX_SUB_ID); 389 } 390 391 /** 392 * Get a new MmsMessage by loading from the cursor of a query 393 * that returns the MMS to import 394 * 395 * @param cursor 396 * @return 397 */ get(final Cursor cursor)398 public static MmsMessage get(final Cursor cursor) { 399 final MmsMessage msg = new MmsMessage(); 400 msg.load(cursor); 401 return msg; 402 } 403 /** 404 * Add a loaded MMS part 405 * 406 * @param part 407 */ addPart(final MmsPart part)408 public void addPart(final MmsPart part) { 409 mParts.add(part); 410 } 411 getParts()412 public List<MmsPart> getParts() { 413 return mParts; 414 } 415 getSize()416 public long getSize() { 417 if (!mPartsProcessed) { 418 processParts(); 419 } 420 return mSize; 421 } 422 423 /** 424 * Process loaded MMS parts to obtain the combined text, the combined attachment url, 425 * the combined content type and the combined size. 426 */ processParts()427 private void processParts() { 428 if (mPartsProcessed) { 429 return; 430 } 431 mPartsProcessed = true; 432 // Remember the width and height of the first media part 433 // These are needed when building attachment list 434 long sizeOfParts = 0L; 435 for (final MmsPart part : mParts) { 436 sizeOfParts += part.mSize; 437 } 438 if (mSize <= 0) { 439 mSize = mSubject != null ? mSubject.getBytes().length : 0L; 440 mSize += sizeOfParts; 441 } 442 } 443 444 @Override getUri()445 public String getUri() { 446 return mUri; 447 } 448 getId()449 public long getId() { 450 return mRowId; 451 } 452 getSubId()453 public int getSubId() { 454 return mSubId; 455 } 456 457 @Override getProtocol()458 public int getProtocol() { 459 return MessageData.PROTOCOL_MMS; 460 } 461 462 @Override getTimestampInMillis()463 public long getTimestampInMillis() { 464 return mTimestampInMillis; 465 } 466 467 @Override describeContents()468 public int describeContents() { 469 return 0; 470 } 471 setSender(final String sender)472 public void setSender(final String sender) { 473 mSender = sender; 474 } 475 MmsMessage(final Parcel in)476 private MmsMessage(final Parcel in) { 477 mUri = in.readString(); 478 mRowId = in.readLong(); 479 mTimestampInMillis = in.readLong(); 480 mSentTimestampInMillis = in.readLong(); 481 mType = in.readInt(); 482 mThreadId = in.readLong(); 483 mStatus = in.readInt(); 484 mRead = in.readInt() != 0; 485 mSeen = in.readInt() != 0; 486 mSubId = in.readInt(); 487 488 // MMS specific 489 mSubject = in.readString(); 490 mContentLocation = in.readString(); 491 mTransactionId = in.readString(); 492 mSender = in.readString(); 493 494 mSize = in.readLong(); 495 mExpiryInMillis = in.readLong(); 496 497 mSubjectCharset = in.readInt(); 498 mPriority = in.readInt(); 499 mMmsMessageType = in.readInt(); 500 mResponseStatus = in.readInt(); 501 mRetrieveStatus = in.readInt(); 502 503 final int nParts = in.readInt(); 504 mParts = new ArrayList<MmsPart>(); 505 mPartsProcessed = false; 506 for (int i = 0; i < nParts; i++) { 507 mParts.add((MmsPart) in.readParcelable(getClass().getClassLoader())); 508 } 509 } 510 511 public static final Parcelable.Creator<MmsMessage> CREATOR 512 = new Parcelable.Creator<MmsMessage>() { 513 @Override 514 public MmsMessage createFromParcel(final Parcel in) { 515 return new MmsMessage(in); 516 } 517 518 @Override 519 public MmsMessage[] newArray(final int size) { 520 return new MmsMessage[size]; 521 } 522 }; 523 524 @Override writeToParcel(final Parcel out, final int flags)525 public void writeToParcel(final Parcel out, final int flags) { 526 out.writeString(mUri); 527 out.writeLong(mRowId); 528 out.writeLong(mTimestampInMillis); 529 out.writeLong(mSentTimestampInMillis); 530 out.writeInt(mType); 531 out.writeLong(mThreadId); 532 out.writeInt(mStatus); 533 out.writeInt(mRead ? 1 : 0); 534 out.writeInt(mSeen ? 1 : 0); 535 out.writeInt(mSubId); 536 537 out.writeString(mSubject); 538 out.writeString(mContentLocation); 539 out.writeString(mTransactionId); 540 out.writeString(mSender); 541 542 out.writeLong(mSize); 543 out.writeLong(mExpiryInMillis); 544 545 out.writeInt(mSubjectCharset); 546 out.writeInt(mPriority); 547 out.writeInt(mMmsMessageType); 548 out.writeInt(mResponseStatus); 549 out.writeInt(mRetrieveStatus); 550 551 out.writeInt(mParts.size()); 552 for (final MmsPart part : mParts) { 553 out.writeParcelable(part, 0); 554 } 555 } 556 } 557 558 /** 559 * Part of an MMS message 560 */ 561 public static class MmsPart implements Parcelable { 562 public static final String[] PROJECTION = new String[] { 563 Mms.Part._ID, 564 Mms.Part.MSG_ID, 565 Mms.Part.CHARSET, 566 Mms.Part.CONTENT_TYPE, 567 Mms.Part.TEXT, 568 }; 569 private static int sIota = 0; 570 public static final int INDEX_ID = sIota++; 571 public static final int INDEX_MSG_ID = sIota++; 572 public static final int INDEX_CHARSET = sIota++; 573 public static final int INDEX_CONTENT_TYPE = sIota++; 574 public static final int INDEX_TEXT = sIota++; 575 576 public String mUri; 577 public long mRowId; 578 public long mMessageId; 579 public String mContentType; 580 public String mText; 581 public int mCharset; 582 private int mWidth; 583 private int mHeight; 584 public long mSize; 585 MmsPart()586 private MmsPart() { 587 } 588 589 /** 590 * Load from a cursor of a query that returns the MMS part to import 591 * 592 * @param cursor 593 */ load(final Cursor cursor, final boolean loadMedia)594 public void load(final Cursor cursor, final boolean loadMedia) { 595 mRowId = cursor.getLong(INDEX_ID); 596 mMessageId = cursor.getLong(INDEX_MSG_ID); 597 mContentType = cursor.getString(INDEX_CONTENT_TYPE); 598 mText = cursor.getString(INDEX_TEXT); 599 mCharset = cursor.getInt(INDEX_CHARSET); 600 mWidth = 0; 601 mHeight = 0; 602 mSize = 0; 603 if (isMedia()) { 604 // For importing we don't load media since performance is critical 605 // For loading when we receive mms, we do load media to get enough 606 // information of the media file 607 if (loadMedia) { 608 if (ContentType.isImageType(mContentType)) { 609 loadImage(); 610 } else if (ContentType.isVideoType(mContentType)) { 611 loadVideo(); 612 } // No need to load audio for parsing 613 mSize = MmsUtils.getMediaFileSize(getDataUri()); 614 } 615 } else { 616 // Load text if not media type 617 loadText(); 618 } 619 mUri = Uri.withAppendedPath(Mms.CONTENT_URI, cursor.getString(INDEX_ID)).toString(); 620 } 621 622 /** 623 * Get content type from file extension 624 */ extractContentType(final Context context, final Uri uri)625 private static String extractContentType(final Context context, final Uri uri) { 626 final String path = uri.getPath(); 627 final MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); 628 String extension = MimeTypeMap.getFileExtensionFromUrl(path); 629 if (TextUtils.isEmpty(extension)) { 630 // getMimeTypeFromExtension() doesn't handle spaces in filenames nor can it handle 631 // urlEncoded strings. Let's try one last time at finding the extension. 632 final int dotPos = path.lastIndexOf('.'); 633 if (0 <= dotPos) { 634 extension = path.substring(dotPos + 1); 635 } 636 } 637 return mimeTypeMap.getMimeTypeFromExtension(extension); 638 } 639 640 /** 641 * Get text of a text part 642 */ loadText()643 private void loadText() { 644 byte[] data = null; 645 if (isEmbeddedTextType()) { 646 // Embedded text, get from the "text" column 647 if (!TextUtils.isEmpty(mText)) { 648 data = getStringBytes(mText, mCharset); 649 } 650 } else { 651 // Not embedded, load from disk 652 final ContentResolver resolver = 653 Factory.get().getApplicationContext().getContentResolver(); 654 final Uri uri = getDataUri(); 655 InputStream is = null; 656 final ByteArrayOutputStream baos = new ByteArrayOutputStream(); 657 try { 658 is = resolver.openInputStream(uri); 659 final byte[] buffer = new byte[256]; 660 int len = is.read(buffer); 661 while (len >= 0) { 662 baos.write(buffer, 0, len); 663 len = is.read(buffer); 664 } 665 } catch (final IOException e) { 666 LogUtil.e(TAG, 667 "DatabaseMessages.MmsPart: loading text from file failed: " + e, e); 668 } finally { 669 if (is != null) { 670 try { 671 is.close(); 672 } catch (final IOException e) { 673 LogUtil.e(TAG, "DatabaseMessages.MmsPart: close file failed: " + e, e); 674 } 675 } 676 } 677 data = baos.toByteArray(); 678 } 679 if (data != null && data.length > 0) { 680 mSize = data.length; 681 mText = getDecodedString(data, mCharset); 682 } 683 } 684 685 /** 686 * Load image file of an image part and parse the dimensions and type 687 */ loadImage()688 private void loadImage() { 689 final Context context = Factory.get().getApplicationContext(); 690 final ContentResolver resolver = context.getContentResolver(); 691 final Uri uri = getDataUri(); 692 // We have to get the width and height of the image -- they're needed when adding 693 // an attachment in bugle. 694 InputStream is = null; 695 try { 696 is = resolver.openInputStream(uri); 697 final BitmapFactory.Options opt = new BitmapFactory.Options(); 698 opt.inJustDecodeBounds = true; 699 BitmapFactory.decodeStream(is, null, opt); 700 mContentType = opt.outMimeType; 701 mWidth = opt.outWidth; 702 mHeight = opt.outHeight; 703 if (TextUtils.isEmpty(mContentType)) { 704 // BitmapFactory couldn't figure out the image type. That's got to be a bad 705 // sign, but see if we can figure it out from the file extension. 706 mContentType = extractContentType(context, uri); 707 } 708 } catch (final FileNotFoundException e) { 709 LogUtil.e(TAG, "DatabaseMessages.MmsPart.loadImage: file not found", e); 710 } finally { 711 if (is != null) { 712 try { 713 is.close(); 714 } catch (final IOException e) { 715 Log.e(TAG, "IOException caught while closing stream", e); 716 } 717 } 718 } 719 } 720 721 /** 722 * Load video file of a video part and parse the dimensions and type 723 */ loadVideo()724 private void loadVideo() { 725 // This is a coarse check, and should not be applied to outgoing messages. However, 726 // currently, this does not cause any problems. 727 if (!VideoThumbnailRequest.shouldShowIncomingVideoThumbnails()) { 728 return; 729 } 730 final Uri uri = getDataUri(); 731 final MediaMetadataRetrieverWrapper retriever = new MediaMetadataRetrieverWrapper(); 732 try { 733 retriever.setDataSource(uri); 734 // FLAG: This inadvertently fixes a problem with phone receiving audio 735 // messages on some carrier. We should handle this in a less accidental way so that 736 // we don't break it again. (The carrier changes the content type in the wrapper 737 // in-transit from audio/mp4 to video/3gpp without changing the data) 738 // Also note: There is a bug in some OEM device where mmr returns 739 // video/ffmpeg for image files. That shouldn't happen here but be aware. 740 mContentType = 741 retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE); 742 final Bitmap bitmap = retriever.getFrameAtTime(-1); 743 if (bitmap != null) { 744 mWidth = bitmap.getWidth(); 745 mHeight = bitmap.getHeight(); 746 } else { 747 // Get here if it's not actually video (see above) 748 LogUtil.i(LogUtil.BUGLE_TAG, "loadVideo: Got null bitmap from " + uri); 749 } 750 } catch (IOException e) { 751 LogUtil.i(LogUtil.BUGLE_TAG, "Error extracting metadata from " + uri, e); 752 } finally { 753 retriever.release(); 754 } 755 } 756 757 /** 758 * Get media file size 759 */ getMediaFileSize()760 private long getMediaFileSize() { 761 final Context context = Factory.get().getApplicationContext(); 762 final Uri uri = getDataUri(); 763 AssetFileDescriptor fd = null; 764 try { 765 fd = context.getContentResolver().openAssetFileDescriptor(uri, "r"); 766 if (fd != null) { 767 return fd.getParcelFileDescriptor().getStatSize(); 768 } 769 } catch (final FileNotFoundException e) { 770 LogUtil.e(TAG, "DatabaseMessages.MmsPart: cound not find media file: " + e, e); 771 } finally { 772 if (fd != null) { 773 try { 774 fd.close(); 775 } catch (final IOException e) { 776 LogUtil.e(TAG, "DatabaseMessages.MmsPart: failed to close " + e, e); 777 } 778 } 779 } 780 return 0L; 781 } 782 783 /** 784 * @return If the type is a text type that stores text embedded (i.e. in db table) 785 */ isEmbeddedTextType()786 private boolean isEmbeddedTextType() { 787 return ContentType.TEXT_PLAIN.equals(mContentType) 788 || ContentType.APP_SMIL.equals(mContentType) 789 || ContentType.TEXT_HTML.equals(mContentType); 790 } 791 792 /** 793 * Get an instance of the MMS part from the part table cursor 794 * 795 * @param cursor 796 * @param loadMedia Whether to load the media file of the part 797 * @return 798 */ get(final Cursor cursor, final boolean loadMedia)799 public static MmsPart get(final Cursor cursor, final boolean loadMedia) { 800 final MmsPart part = new MmsPart(); 801 part.load(cursor, loadMedia); 802 return part; 803 } 804 isText()805 public boolean isText() { 806 return ContentType.TEXT_PLAIN.equals(mContentType) 807 || ContentType.TEXT_HTML.equals(mContentType) 808 || ContentType.APP_WAP_XHTML.equals(mContentType); 809 } 810 isMedia()811 public boolean isMedia() { 812 return ContentType.isImageType(mContentType) 813 || ContentType.isVideoType(mContentType) 814 || ContentType.isAudioType(mContentType) 815 || ContentType.isVCardType(mContentType); 816 } 817 isImage()818 public boolean isImage() { 819 return ContentType.isImageType(mContentType); 820 } 821 getDataUri()822 public Uri getDataUri() { 823 return Uri.parse("content://mms/part/" + mRowId); 824 } 825 826 @Override describeContents()827 public int describeContents() { 828 return 0; 829 } 830 MmsPart(final Parcel in)831 private MmsPart(final Parcel in) { 832 mUri = in.readString(); 833 mRowId = in.readLong(); 834 mMessageId = in.readLong(); 835 mContentType = in.readString(); 836 mText = in.readString(); 837 mCharset = in.readInt(); 838 mWidth = in.readInt(); 839 mHeight = in.readInt(); 840 mSize = in.readLong(); 841 } 842 843 public static final Parcelable.Creator<MmsPart> CREATOR 844 = new Parcelable.Creator<MmsPart>() { 845 @Override 846 public MmsPart createFromParcel(final Parcel in) { 847 return new MmsPart(in); 848 } 849 850 @Override 851 public MmsPart[] newArray(final int size) { 852 return new MmsPart[size]; 853 } 854 }; 855 856 @Override writeToParcel(final Parcel out, final int flags)857 public void writeToParcel(final Parcel out, final int flags) { 858 out.writeString(mUri); 859 out.writeLong(mRowId); 860 out.writeLong(mMessageId); 861 out.writeString(mContentType); 862 out.writeString(mText); 863 out.writeInt(mCharset); 864 out.writeInt(mWidth); 865 out.writeInt(mHeight); 866 out.writeLong(mSize); 867 } 868 } 869 870 /** 871 * This class provides the same DatabaseMessage interface over a local SMS db message 872 */ 873 public static class LocalDatabaseMessage extends DatabaseMessage implements Parcelable { 874 private final int mProtocol; 875 private final String mUri; 876 private final long mTimestamp; 877 private final long mLocalId; 878 private final String mConversationId; 879 LocalDatabaseMessage(final long localId, final int protocol, final String uri, final long timestamp, final String conversationId)880 public LocalDatabaseMessage(final long localId, final int protocol, final String uri, 881 final long timestamp, final String conversationId) { 882 mLocalId = localId; 883 mProtocol = protocol; 884 mUri = uri; 885 mTimestamp = timestamp; 886 mConversationId = conversationId; 887 } 888 889 @Override getProtocol()890 public int getProtocol() { 891 return mProtocol; 892 } 893 894 @Override getTimestampInMillis()895 public long getTimestampInMillis() { 896 return mTimestamp; 897 } 898 899 @Override getUri()900 public String getUri() { 901 return mUri; 902 } 903 getLocalId()904 public long getLocalId() { 905 return mLocalId; 906 } 907 getConversationId()908 public String getConversationId() { 909 return mConversationId; 910 } 911 912 @Override describeContents()913 public int describeContents() { 914 return 0; 915 } 916 LocalDatabaseMessage(final Parcel in)917 private LocalDatabaseMessage(final Parcel in) { 918 mUri = in.readString(); 919 mConversationId = in.readString(); 920 mLocalId = in.readLong(); 921 mTimestamp = in.readLong(); 922 mProtocol = in.readInt(); 923 } 924 925 public static final Parcelable.Creator<LocalDatabaseMessage> CREATOR 926 = new Parcelable.Creator<LocalDatabaseMessage>() { 927 @Override 928 public LocalDatabaseMessage createFromParcel(final Parcel in) { 929 return new LocalDatabaseMessage(in); 930 } 931 932 @Override 933 public LocalDatabaseMessage[] newArray(final int size) { 934 return new LocalDatabaseMessage[size]; 935 } 936 }; 937 938 @Override writeToParcel(final Parcel out, final int flags)939 public void writeToParcel(final Parcel out, final int flags) { 940 out.writeString(mUri); 941 out.writeString(mConversationId); 942 out.writeLong(mLocalId); 943 out.writeLong(mTimestamp); 944 out.writeInt(mProtocol); 945 } 946 } 947 948 /** 949 * Address for MMS message 950 */ 951 public static class MmsAddr { 952 public static final String[] PROJECTION = new String[] { 953 Mms.Addr.ADDRESS, 954 Mms.Addr.CHARSET, 955 }; 956 private static int sIota = 0; 957 public static final int INDEX_ADDRESS = sIota++; 958 public static final int INDEX_CHARSET = sIota++; 959 get(final Cursor cursor)960 public static String get(final Cursor cursor) { 961 final int charset = cursor.getInt(INDEX_CHARSET); 962 // PduPersister stores the addresses using ISO_8859_1 963 // Let's load it using that encoding and convert it back to its original 964 // See PduPersister.persistAddress 965 return getDecodedString( 966 getStringBytes(cursor.getString(INDEX_ADDRESS), CharacterSets.ISO_8859_1), 967 charset); 968 } 969 } 970 971 /** 972 * Decoded string by character set 973 */ getDecodedString(final byte[] data, final int charset)974 public static String getDecodedString(final byte[] data, final int charset) { 975 if (CharacterSets.ANY_CHARSET == charset) { 976 return new String(data); // system default encoding. 977 } else { 978 try { 979 final String name = CharacterSets.getMimeName(charset); 980 return new String(data, name); 981 } catch (final UnsupportedEncodingException e) { 982 try { 983 return new String(data, CharacterSets.MIMENAME_ISO_8859_1); 984 } catch (final UnsupportedEncodingException exception) { 985 return new String(data); // system default encoding. 986 } 987 } 988 } 989 } 990 991 /** 992 * Unpack a given String into a byte[]. 993 */ getStringBytes(final String data, final int charset)994 public static byte[] getStringBytes(final String data, final int charset) { 995 if (CharacterSets.ANY_CHARSET == charset) { 996 return data.getBytes(); 997 } else { 998 try { 999 final String name = CharacterSets.getMimeName(charset); 1000 return data.getBytes(name); 1001 } catch (final UnsupportedEncodingException e) { 1002 return data.getBytes(); 1003 } 1004 } 1005 } 1006 } 1007