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 { 63 public abstract int getProtocol(); 64 public abstract String getUri(); 65 public abstract long getTimestampInMillis(); 66 67 @Override 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 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 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 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 */ 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 */ 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 187 public String getUri() { 188 return mUri; 189 } 190 191 public int getSubId() { 192 return mSubId; 193 } 194 195 @Override 196 public int getProtocol() { 197 return MessageData.PROTOCOL_SMS; 198 } 199 200 @Override 201 public long getTimestampInMillis() { 202 return mTimestampInMillis; 203 } 204 205 @Override 206 public int describeContents() { 207 return 0; 208 } 209 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 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 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 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 */ 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 */ 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 */ 408 public void addPart(final MmsPart part) { 409 mParts.add(part); 410 } 411 412 public List<MmsPart> getParts() { 413 return mParts; 414 } 415 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 */ 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 445 public String getUri() { 446 return mUri; 447 } 448 449 public long getId() { 450 return mRowId; 451 } 452 453 public int getSubId() { 454 return mSubId; 455 } 456 457 @Override 458 public int getProtocol() { 459 return MessageData.PROTOCOL_MMS; 460 } 461 462 @Override 463 public long getTimestampInMillis() { 464 return mTimestampInMillis; 465 } 466 467 @Override 468 public int describeContents() { 469 return 0; 470 } 471 472 public void setSender(final String sender) { 473 mSender = sender; 474 } 475 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 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 811 public boolean isMedia() { 812 return ContentType.isImageType(mContentType) 813 || ContentType.isVideoType(mContentType) 814 || ContentType.isAudioType(mContentType) 815 || ContentType.isVCardType(mContentType); 816 } 817 818 public boolean isImage() { 819 return ContentType.isImageType(mContentType); 820 } 821 822 public Uri getDataUri() { 823 return Uri.parse("content://mms/part/" + mRowId); 824 } 825 826 @Override 827 public int describeContents() { 828 return 0; 829 } 830 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 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 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 890 public int getProtocol() { 891 return mProtocol; 892 } 893 894 @Override 895 public long getTimestampInMillis() { 896 return mTimestamp; 897 } 898 899 @Override 900 public String getUri() { 901 return mUri; 902 } 903 904 public long getLocalId() { 905 return mLocalId; 906 } 907 908 public String getConversationId() { 909 return mConversationId; 910 } 911 912 @Override 913 public int describeContents() { 914 return 0; 915 } 916 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 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 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 */ 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 */ 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