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