1 /* 2 * Copyright (C) 2008 Esmertec AG. 3 * Copyright (C) 2008 The Android Open Source Project 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package com.android.mms.model; 19 20 21 import java.io.ByteArrayOutputStream; 22 import java.io.FileNotFoundException; 23 import java.io.IOException; 24 import java.io.InputStream; 25 import java.util.ArrayList; 26 import java.util.Collection; 27 import java.util.HashMap; 28 import java.util.Iterator; 29 import java.util.List; 30 import java.util.ListIterator; 31 32 import org.w3c.dom.NodeList; 33 import org.w3c.dom.events.EventTarget; 34 import org.w3c.dom.smil.SMILDocument; 35 import org.w3c.dom.smil.SMILElement; 36 import org.w3c.dom.smil.SMILLayoutElement; 37 import org.w3c.dom.smil.SMILMediaElement; 38 import org.w3c.dom.smil.SMILParElement; 39 import org.w3c.dom.smil.SMILRegionElement; 40 import org.w3c.dom.smil.SMILRootLayoutElement; 41 42 import android.content.ContentResolver; 43 import android.content.ContentUris; 44 import android.content.Context; 45 import android.net.Uri; 46 import android.text.TextUtils; 47 import android.util.Log; 48 49 import com.android.mms.ContentRestrictionException; 50 import com.android.mms.ExceedMessageSizeException; 51 import com.android.mms.LogTag; 52 import com.android.mms.MmsConfig; 53 import com.android.mms.dom.smil.parser.SmilXmlSerializer; 54 import com.android.mms.layout.LayoutManager; 55 import com.google.android.mms.ContentType; 56 import com.google.android.mms.MmsException; 57 import com.google.android.mms.pdu.GenericPdu; 58 import com.google.android.mms.pdu.MultimediaMessagePdu; 59 import com.google.android.mms.pdu.PduBody; 60 import com.google.android.mms.pdu.PduHeaders; 61 import com.google.android.mms.pdu.PduPart; 62 import com.google.android.mms.pdu.PduPersister; 63 import com.android.mms.UnsupportContentTypeException; 64 65 public class SlideshowModel extends Model 66 implements List<SlideModel>, IModelChangedObserver { 67 private static final String TAG = LogTag.TAG; 68 69 private final LayoutModel mLayout; 70 private final ArrayList<SlideModel> mSlides; 71 private SMILDocument mDocumentCache; 72 private PduBody mPduBodyCache; 73 private int mCurrentMessageSize; // This is the current message size, not including 74 // attachments that can be resized (such as photos) 75 private int mTotalMessageSize; // This is the computed total message size 76 private Context mContext; 77 78 // amount of space to leave in a slideshow for text and overhead. 79 public static final int SLIDESHOW_SLOP = 1024; 80 81 private SlideshowModel(Context context) { 82 mLayout = new LayoutModel(); 83 mSlides = new ArrayList<SlideModel>(); 84 mContext = context; 85 } 86 87 private SlideshowModel ( 88 LayoutModel layouts, ArrayList<SlideModel> slides, 89 SMILDocument documentCache, PduBody pbCache, 90 Context context) { 91 mLayout = layouts; 92 mSlides = slides; 93 mContext = context; 94 95 mDocumentCache = documentCache; 96 mPduBodyCache = pbCache; 97 for (SlideModel slide : mSlides) { 98 increaseMessageSize(slide.getSlideSize()); 99 slide.setParent(this); 100 } 101 } 102 103 public static SlideshowModel createNew(Context context) { 104 return new SlideshowModel(context); 105 } 106 107 public static SlideshowModel createFromMessageUri( 108 Context context, Uri uri) throws MmsException { 109 return createFromPduBody(context, getPduBody(context, uri)); 110 } 111 112 public static SlideshowModel createFromPduBody(Context context, PduBody pb) throws MmsException { 113 SMILDocument document = SmilHelper.getDocument(pb); 114 115 // Create root-layout model. 116 SMILLayoutElement sle = document.getLayout(); 117 SMILRootLayoutElement srle = sle.getRootLayout(); 118 int w = srle.getWidth(); 119 int h = srle.getHeight(); 120 if ((w == 0) || (h == 0)) { 121 w = LayoutManager.getInstance().getLayoutParameters().getWidth(); 122 h = LayoutManager.getInstance().getLayoutParameters().getHeight(); 123 srle.setWidth(w); 124 srle.setHeight(h); 125 } 126 RegionModel rootLayout = new RegionModel( 127 null, 0, 0, w, h); 128 129 // Create region models. 130 ArrayList<RegionModel> regions = new ArrayList<RegionModel>(); 131 NodeList nlRegions = sle.getRegions(); 132 int regionsNum = nlRegions.getLength(); 133 134 for (int i = 0; i < regionsNum; i++) { 135 SMILRegionElement sre = (SMILRegionElement) nlRegions.item(i); 136 RegionModel r = new RegionModel(sre.getId(), sre.getFit(), 137 sre.getLeft(), sre.getTop(), sre.getWidth(), sre.getHeight(), 138 sre.getBackgroundColor()); 139 regions.add(r); 140 } 141 LayoutModel layouts = new LayoutModel(rootLayout, regions); 142 143 // Create slide models. 144 SMILElement docBody = document.getBody(); 145 NodeList slideNodes = docBody.getChildNodes(); 146 int slidesNum = slideNodes.getLength(); 147 ArrayList<SlideModel> slides = new ArrayList<SlideModel>(slidesNum); 148 int totalMessageSize = 0; 149 150 for (int i = 0; i < slidesNum; i++) { 151 // FIXME: This is NOT compatible with the SMILDocument which is 152 // generated by some other mobile phones. 153 SMILParElement par = (SMILParElement) slideNodes.item(i); 154 155 // Create media models for each slide. 156 NodeList mediaNodes = par.getChildNodes(); 157 int mediaNum = mediaNodes.getLength(); 158 ArrayList<MediaModel> mediaSet = new ArrayList<MediaModel>(mediaNum); 159 160 for (int j = 0; j < mediaNum; j++) { 161 SMILMediaElement sme = (SMILMediaElement) mediaNodes.item(j); 162 try { 163 MediaModel media = MediaModelFactory.getMediaModel( 164 context, sme, layouts, pb); 165 166 /* 167 * This is for slide duration value set. 168 * If mms server does not support slide duration. 169 */ 170 if (!MmsConfig.getSlideDurationEnabled()) { 171 int mediadur = media.getDuration(); 172 float dur = par.getDur(); 173 if (dur == 0) { 174 mediadur = MmsConfig.getMinimumSlideElementDuration() * 1000; 175 media.setDuration(mediadur); 176 } 177 178 if ((int)mediadur / 1000 != dur) { 179 String tag = sme.getTagName(); 180 181 if (ContentType.isVideoType(media.mContentType) 182 || tag.equals(SmilHelper.ELEMENT_TAG_VIDEO) 183 || ContentType.isAudioType(media.mContentType) 184 || tag.equals(SmilHelper.ELEMENT_TAG_AUDIO)) { 185 /* 186 * add 1 sec to release and close audio/video 187 * for guaranteeing the audio/video playing. 188 * because the mmsc does not support the slide duration. 189 */ 190 par.setDur((float)mediadur / 1000 + 1); 191 } else { 192 /* 193 * If a slide has an image and an audio/video element 194 * and the audio/video element has longer duration than the image, 195 * The Image disappear before the slide play done. so have to match 196 * an image duration to the slide duration. 197 */ 198 if ((int)mediadur / 1000 < dur) { 199 media.setDuration((int)dur * 1000); 200 } else { 201 if ((int)dur != 0) { 202 media.setDuration((int)dur * 1000); 203 } else { 204 par.setDur((float)mediadur / 1000); 205 } 206 } 207 } 208 } 209 } 210 SmilHelper.addMediaElementEventListeners( 211 (EventTarget) sme, media); 212 mediaSet.add(media); 213 totalMessageSize += media.getMediaSize(); 214 } catch (IOException e) { 215 Log.e(TAG, e.getMessage(), e); 216 } catch (IllegalArgumentException e) { 217 Log.e(TAG, e.getMessage(), e); 218 } catch (UnsupportContentTypeException e) { 219 Log.e(TAG, e.getMessage(), e); 220 } 221 } 222 223 SlideModel slide = new SlideModel((int) (par.getDur() * 1000), mediaSet); 224 slide.setFill(par.getFill()); 225 SmilHelper.addParElementEventListeners((EventTarget) par, slide); 226 slides.add(slide); 227 } 228 229 SlideshowModel slideshow = new SlideshowModel(layouts, slides, document, pb, context); 230 slideshow.mTotalMessageSize = totalMessageSize; 231 slideshow.registerModelChangedObserver(slideshow); 232 return slideshow; 233 } 234 235 public PduBody toPduBody() { 236 if (mPduBodyCache == null) { 237 mDocumentCache = SmilHelper.getDocument(this); 238 mPduBodyCache = makePduBody(mDocumentCache); 239 } 240 return mPduBodyCache; 241 } 242 243 private PduBody makePduBody(SMILDocument document) { 244 PduBody pb = new PduBody(); 245 246 boolean hasForwardLock = false; 247 for (SlideModel slide : mSlides) { 248 for (MediaModel media : slide) { 249 PduPart part = new PduPart(); 250 251 if (media.isText()) { 252 TextModel text = (TextModel) media; 253 // Don't create empty text part. 254 if (TextUtils.isEmpty(text.getText())) { 255 continue; 256 } 257 // Set Charset if it's a text media. 258 part.setCharset(text.getCharset()); 259 } 260 261 // Set Content-Type. 262 part.setContentType(media.getContentType().getBytes()); 263 264 String src = media.getSrc(); 265 String location; 266 boolean startWithContentId = src.startsWith("cid:"); 267 if (startWithContentId) { 268 location = src.substring("cid:".length()); 269 } else { 270 location = src; 271 } 272 273 // Set Content-Location. 274 part.setContentLocation(location.getBytes()); 275 276 // Set Content-Id. 277 if (startWithContentId) { 278 //Keep the original Content-Id. 279 part.setContentId(location.getBytes()); 280 } 281 else { 282 int index = location.lastIndexOf("."); 283 String contentId = (index == -1) ? location 284 : location.substring(0, index); 285 part.setContentId(contentId.getBytes()); 286 } 287 288 if (media.isText()) { 289 part.setData(((TextModel) media).getText().getBytes()); 290 } else if (media.isImage() || media.isVideo() || media.isAudio()) { 291 part.setDataUri(media.getUri()); 292 } else { 293 Log.w(TAG, "Unsupport media: " + media); 294 } 295 296 pb.addPart(part); 297 } 298 } 299 300 // Create and insert SMIL part(as the first part) into the PduBody. 301 ByteArrayOutputStream out = new ByteArrayOutputStream(); 302 SmilXmlSerializer.serialize(document, out); 303 PduPart smilPart = new PduPart(); 304 smilPart.setContentId("smil".getBytes()); 305 smilPart.setContentLocation("smil.xml".getBytes()); 306 smilPart.setContentType(ContentType.APP_SMIL.getBytes()); 307 smilPart.setData(out.toByteArray()); 308 pb.addPart(0, smilPart); 309 310 return pb; 311 } 312 313 public HashMap<Uri, InputStream> openPartFiles(ContentResolver cr) { 314 HashMap<Uri, InputStream> openedFiles = null; // Don't create unless we have to 315 316 for (SlideModel slide : mSlides) { 317 for (MediaModel media : slide) { 318 if (media.isText()) { 319 continue; 320 } 321 Uri uri = media.getUri(); 322 InputStream is; 323 try { 324 is = cr.openInputStream(uri); 325 if (is != null) { 326 if (openedFiles == null) { 327 openedFiles = new HashMap<Uri, InputStream>(); 328 } 329 openedFiles.put(uri, is); 330 } 331 } catch (FileNotFoundException e) { 332 Log.e(TAG, "openPartFiles couldn't open: " + uri, e); 333 } 334 } 335 } 336 return openedFiles; 337 } 338 339 public PduBody makeCopy() { 340 return makePduBody(SmilHelper.getDocument(this)); 341 } 342 343 public SMILDocument toSmilDocument() { 344 if (mDocumentCache == null) { 345 mDocumentCache = SmilHelper.getDocument(this); 346 } 347 return mDocumentCache; 348 } 349 350 public static PduBody getPduBody(Context context, Uri msg) throws MmsException { 351 PduPersister p = PduPersister.getPduPersister(context); 352 GenericPdu pdu = p.load(msg); 353 354 int msgType = pdu.getMessageType(); 355 if ((msgType == PduHeaders.MESSAGE_TYPE_SEND_REQ) 356 || (msgType == PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF)) { 357 return ((MultimediaMessagePdu) pdu).getBody(); 358 } else { 359 throw new MmsException(); 360 } 361 } 362 363 public void setCurrentMessageSize(int size) { 364 mCurrentMessageSize = size; 365 } 366 367 // getCurrentMessageSize returns the size of the message, not including resizable attachments 368 // such as photos. mCurrentMessageSize is used when adding/deleting/replacing non-resizable 369 // attachments (movies, sounds, etc) in order to compute how much size is left in the message. 370 // The difference between mCurrentMessageSize and the maxSize allowed for a message is then 371 // divided up between the remaining resizable attachments. While this function is public, 372 // it is only used internally between various MMS classes. If the UI wants to know the 373 // size of a MMS message, it should call getTotalMessageSize() instead. 374 public int getCurrentMessageSize() { 375 return mCurrentMessageSize; 376 } 377 378 // getTotalMessageSize returns the total size of the message, including resizable attachments 379 // such as photos. This function is intended to be used by the UI for displaying the size of the 380 // MMS message. 381 public int getTotalMessageSize() { 382 return mTotalMessageSize; 383 } 384 385 public void increaseMessageSize(int increaseSize) { 386 if (increaseSize > 0) { 387 mCurrentMessageSize += increaseSize; 388 } 389 } 390 391 public void decreaseMessageSize(int decreaseSize) { 392 if (decreaseSize > 0) { 393 mCurrentMessageSize -= decreaseSize; 394 } 395 } 396 397 public LayoutModel getLayout() { 398 return mLayout; 399 } 400 401 // 402 // Implement List<E> interface. 403 // 404 public boolean add(SlideModel object) { 405 int increaseSize = object.getSlideSize(); 406 checkMessageSize(increaseSize); 407 408 if ((object != null) && mSlides.add(object)) { 409 increaseMessageSize(increaseSize); 410 object.registerModelChangedObserver(this); 411 for (IModelChangedObserver observer : mModelChangedObservers) { 412 object.registerModelChangedObserver(observer); 413 } 414 notifyModelChanged(true); 415 return true; 416 } 417 return false; 418 } 419 420 public boolean addAll(Collection<? extends SlideModel> collection) { 421 throw new UnsupportedOperationException("Operation not supported."); 422 } 423 424 public void clear() { 425 if (mSlides.size() > 0) { 426 for (SlideModel slide : mSlides) { 427 slide.unregisterModelChangedObserver(this); 428 for (IModelChangedObserver observer : mModelChangedObservers) { 429 slide.unregisterModelChangedObserver(observer); 430 } 431 } 432 mCurrentMessageSize = 0; 433 mSlides.clear(); 434 notifyModelChanged(true); 435 } 436 } 437 438 public boolean contains(Object object) { 439 return mSlides.contains(object); 440 } 441 442 public boolean containsAll(Collection<?> collection) { 443 return mSlides.containsAll(collection); 444 } 445 446 public boolean isEmpty() { 447 return mSlides.isEmpty(); 448 } 449 450 public Iterator<SlideModel> iterator() { 451 return mSlides.iterator(); 452 } 453 454 public boolean remove(Object object) { 455 if ((object != null) && mSlides.remove(object)) { 456 SlideModel slide = (SlideModel) object; 457 decreaseMessageSize(slide.getSlideSize()); 458 slide.unregisterAllModelChangedObservers(); 459 notifyModelChanged(true); 460 return true; 461 } 462 return false; 463 } 464 465 public boolean removeAll(Collection<?> collection) { 466 throw new UnsupportedOperationException("Operation not supported."); 467 } 468 469 public boolean retainAll(Collection<?> collection) { 470 throw new UnsupportedOperationException("Operation not supported."); 471 } 472 473 public int size() { 474 return mSlides.size(); 475 } 476 477 public Object[] toArray() { 478 return mSlides.toArray(); 479 } 480 481 public <T> T[] toArray(T[] array) { 482 return mSlides.toArray(array); 483 } 484 485 public void add(int location, SlideModel object) { 486 if (object != null) { 487 int increaseSize = object.getSlideSize(); 488 checkMessageSize(increaseSize); 489 490 mSlides.add(location, object); 491 increaseMessageSize(increaseSize); 492 object.registerModelChangedObserver(this); 493 for (IModelChangedObserver observer : mModelChangedObservers) { 494 object.registerModelChangedObserver(observer); 495 } 496 notifyModelChanged(true); 497 } 498 } 499 500 public boolean addAll(int location, 501 Collection<? extends SlideModel> collection) { 502 throw new UnsupportedOperationException("Operation not supported."); 503 } 504 505 public SlideModel get(int location) { 506 return (location >= 0 && location < mSlides.size()) ? mSlides.get(location) : null; 507 } 508 509 public int indexOf(Object object) { 510 return mSlides.indexOf(object); 511 } 512 513 public int lastIndexOf(Object object) { 514 return mSlides.lastIndexOf(object); 515 } 516 517 public ListIterator<SlideModel> listIterator() { 518 return mSlides.listIterator(); 519 } 520 521 public ListIterator<SlideModel> listIterator(int location) { 522 return mSlides.listIterator(location); 523 } 524 525 public SlideModel remove(int location) { 526 SlideModel slide = mSlides.remove(location); 527 if (slide != null) { 528 decreaseMessageSize(slide.getSlideSize()); 529 slide.unregisterAllModelChangedObservers(); 530 notifyModelChanged(true); 531 } 532 return slide; 533 } 534 535 public SlideModel set(int location, SlideModel object) { 536 SlideModel slide = mSlides.get(location); 537 if (null != object) { 538 int removeSize = 0; 539 int addSize = object.getSlideSize(); 540 if (null != slide) { 541 removeSize = slide.getSlideSize(); 542 } 543 if (addSize > removeSize) { 544 checkMessageSize(addSize - removeSize); 545 increaseMessageSize(addSize - removeSize); 546 } else { 547 decreaseMessageSize(removeSize - addSize); 548 } 549 } 550 551 slide = mSlides.set(location, object); 552 if (slide != null) { 553 slide.unregisterAllModelChangedObservers(); 554 } 555 556 if (object != null) { 557 object.registerModelChangedObserver(this); 558 for (IModelChangedObserver observer : mModelChangedObservers) { 559 object.registerModelChangedObserver(observer); 560 } 561 } 562 563 notifyModelChanged(true); 564 return slide; 565 } 566 567 public List<SlideModel> subList(int start, int end) { 568 return mSlides.subList(start, end); 569 } 570 571 @Override 572 protected void registerModelChangedObserverInDescendants( 573 IModelChangedObserver observer) { 574 mLayout.registerModelChangedObserver(observer); 575 576 for (SlideModel slide : mSlides) { 577 slide.registerModelChangedObserver(observer); 578 } 579 } 580 581 @Override 582 protected void unregisterModelChangedObserverInDescendants( 583 IModelChangedObserver observer) { 584 mLayout.unregisterModelChangedObserver(observer); 585 586 for (SlideModel slide : mSlides) { 587 slide.unregisterModelChangedObserver(observer); 588 } 589 } 590 591 @Override 592 protected void unregisterAllModelChangedObserversInDescendants() { 593 mLayout.unregisterAllModelChangedObservers(); 594 595 for (SlideModel slide : mSlides) { 596 slide.unregisterAllModelChangedObservers(); 597 } 598 } 599 600 public void onModelChanged(Model model, boolean dataChanged) { 601 if (dataChanged) { 602 mDocumentCache = null; 603 mPduBodyCache = null; 604 } 605 } 606 607 public void sync(PduBody pb) { 608 for (SlideModel slide : mSlides) { 609 for (MediaModel media : slide) { 610 PduPart part = pb.getPartByContentLocation(media.getSrc()); 611 if (part != null) { 612 media.setUri(part.getDataUri()); 613 } 614 } 615 } 616 } 617 618 public void checkMessageSize(int increaseSize) throws ContentRestrictionException { 619 ContentRestriction cr = ContentRestrictionFactory.getContentRestriction(); 620 cr.checkMessageSize(mCurrentMessageSize, increaseSize, mContext.getContentResolver()); 621 } 622 623 /** 624 * Determines whether this is a "simple" slideshow. 625 * Criteria: 626 * - Exactly one slide 627 * - Exactly one multimedia attachment, but no audio 628 * - It can optionally have a caption 629 */ 630 public boolean isSimple() { 631 // There must be one (and only one) slide. 632 if (size() != 1) 633 return false; 634 635 SlideModel slide = get(0); 636 // The slide must have either an image or video, but not both. 637 if (!(slide.hasImage() ^ slide.hasVideo())) 638 return false; 639 640 // No audio allowed. 641 if (slide.hasAudio()) 642 return false; 643 644 return true; 645 } 646 647 /** 648 * Make sure the text in slide 0 is no longer holding onto a reference to the text 649 * in the message text box. 650 */ 651 public void prepareForSend() { 652 if (size() == 1) { 653 TextModel text = get(0).getText(); 654 if (text != null) { 655 text.cloneText(); 656 } 657 } 658 } 659 660 /** 661 * Resize all the resizeable media objects to fit in the remaining size of the slideshow. 662 * This should be called off of the UI thread. 663 * 664 * @throws MmsException, ExceedMessageSizeException 665 */ 666 public void finalResize(Uri messageUri) throws MmsException, ExceedMessageSizeException { 667 668 // Figure out if we have any media items that need to be resized and total up the 669 // sizes of the items that can't be resized. 670 int resizableCnt = 0; 671 int fixedSizeTotal = 0; 672 for (SlideModel slide : mSlides) { 673 for (MediaModel media : slide) { 674 if (media.getMediaResizable()) { 675 ++resizableCnt; 676 } else { 677 fixedSizeTotal += media.getMediaSize(); 678 } 679 } 680 } 681 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 682 Log.v(TAG, "finalResize: original message size: " + getCurrentMessageSize() + 683 " getMaxMessageSize: " + MmsConfig.getMaxMessageSize() + 684 " fixedSizeTotal: " + fixedSizeTotal); 685 } 686 if (resizableCnt > 0) { 687 int remainingSize = MmsConfig.getMaxMessageSize() - fixedSizeTotal - SLIDESHOW_SLOP; 688 if (remainingSize <= 0) { 689 throw new ExceedMessageSizeException("No room for pictures"); 690 } 691 long messageId = ContentUris.parseId(messageUri); 692 int bytesPerMediaItem = remainingSize / resizableCnt; 693 // Resize the resizable media items to fit within their byte limit. 694 for (SlideModel slide : mSlides) { 695 for (MediaModel media : slide) { 696 if (media.getMediaResizable()) { 697 media.resizeMedia(bytesPerMediaItem, messageId); 698 } 699 } 700 } 701 // One last time through to calc the real message size. 702 int totalSize = 0; 703 for (SlideModel slide : mSlides) { 704 for (MediaModel media : slide) { 705 totalSize += media.getMediaSize(); 706 } 707 } 708 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 709 Log.v(TAG, "finalResize: new message size: " + totalSize); 710 } 711 712 if (totalSize > MmsConfig.getMaxMessageSize()) { 713 throw new ExceedMessageSizeException("After compressing pictures, message too big"); 714 } 715 setCurrentMessageSize(totalSize); 716 717 onModelChanged(this, true); // clear the cached pdu body 718 PduBody pb = toPduBody(); 719 // This will write out all the new parts to: 720 // /data/data/com.android.providers.telephony/app_parts 721 // and at the same time delete the old parts. 722 PduPersister.getPduPersister(mContext).updateParts(messageUri, pb, null); 723 } 724 } 725 726 } 727