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 com.android.mms.ContentRestrictionException; 22 import com.android.mms.ExceedMessageSizeException; 23 import com.android.mms.LogTag; 24 import com.android.mms.MmsConfig; 25 import com.android.mms.R; 26 import com.android.mms.dom.smil.parser.SmilXmlSerializer; 27 import android.drm.mobile1.DrmException; 28 import com.android.mms.drm.DrmWrapper; 29 import com.android.mms.layout.LayoutManager; 30 import com.google.android.mms.ContentType; 31 import com.google.android.mms.MmsException; 32 import com.google.android.mms.pdu.GenericPdu; 33 import com.google.android.mms.pdu.MultimediaMessagePdu; 34 import com.google.android.mms.pdu.PduBody; 35 import com.google.android.mms.pdu.PduHeaders; 36 import com.google.android.mms.pdu.PduPart; 37 import com.google.android.mms.pdu.PduPersister; 38 39 import org.w3c.dom.NodeList; 40 import org.w3c.dom.events.EventTarget; 41 import org.w3c.dom.smil.SMILDocument; 42 import org.w3c.dom.smil.SMILElement; 43 import org.w3c.dom.smil.SMILLayoutElement; 44 import org.w3c.dom.smil.SMILMediaElement; 45 import org.w3c.dom.smil.SMILParElement; 46 import org.w3c.dom.smil.SMILRegionElement; 47 import org.w3c.dom.smil.SMILRootLayoutElement; 48 49 import android.content.ContentUris; 50 import android.content.Context; 51 import android.net.Uri; 52 import android.text.TextUtils; 53 import android.util.Log; 54 import android.widget.Toast; 55 56 import java.io.ByteArrayOutputStream; 57 import java.io.IOException; 58 import java.util.ArrayList; 59 import java.util.Collection; 60 import java.util.Iterator; 61 import java.util.List; 62 import java.util.ListIterator; 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 (DrmException e) { 214 Log.e(TAG, e.getMessage(), e); 215 } catch (IOException e) { 216 Log.e(TAG, e.getMessage(), e); 217 } catch (IllegalArgumentException e) { 218 Log.e(TAG, e.getMessage(), e); 219 } 220 } 221 222 SlideModel slide = new SlideModel((int) (par.getDur() * 1000), mediaSet); 223 slide.setFill(par.getFill()); 224 SmilHelper.addParElementEventListeners((EventTarget) par, slide); 225 slides.add(slide); 226 } 227 228 SlideshowModel slideshow = new SlideshowModel(layouts, slides, document, pb, context); 229 slideshow.mTotalMessageSize = totalMessageSize; 230 slideshow.registerModelChangedObserver(slideshow); 231 return slideshow; 232 } 233 234 public PduBody toPduBody() { 235 if (mPduBodyCache == null) { 236 mDocumentCache = SmilHelper.getDocument(this); 237 mPduBodyCache = makePduBody(mDocumentCache); 238 } 239 return mPduBodyCache; 240 } 241 242 private PduBody makePduBody(SMILDocument document) { 243 return makePduBody(null, document, false); 244 } 245 246 private PduBody makePduBody(Context context, SMILDocument document, boolean isMakingCopy) { 247 PduBody pb = new PduBody(); 248 249 boolean hasForwardLock = false; 250 for (SlideModel slide : mSlides) { 251 for (MediaModel media : slide) { 252 if (isMakingCopy) { 253 if (media.isDrmProtected() && !media.isAllowedToForward()) { 254 hasForwardLock = true; 255 continue; 256 } 257 } 258 259 PduPart part = new PduPart(); 260 261 if (media.isText()) { 262 TextModel text = (TextModel) media; 263 // Don't create empty text part. 264 if (TextUtils.isEmpty(text.getText())) { 265 continue; 266 } 267 // Set Charset if it's a text media. 268 part.setCharset(text.getCharset()); 269 } 270 271 // Set Content-Type. 272 part.setContentType(media.getContentType().getBytes()); 273 274 String src = media.getSrc(); 275 String location; 276 boolean startWithContentId = src.startsWith("cid:"); 277 if (startWithContentId) { 278 location = src.substring("cid:".length()); 279 } else { 280 location = src; 281 } 282 283 // Set Content-Location. 284 part.setContentLocation(location.getBytes()); 285 286 // Set Content-Id. 287 if (startWithContentId) { 288 //Keep the original Content-Id. 289 part.setContentId(location.getBytes()); 290 } 291 else { 292 int index = location.lastIndexOf("."); 293 String contentId = (index == -1) ? location 294 : location.substring(0, index); 295 part.setContentId(contentId.getBytes()); 296 } 297 298 if (media.isDrmProtected()) { 299 DrmWrapper wrapper = media.getDrmObject(); 300 part.setDataUri(wrapper.getOriginalUri()); 301 part.setData(wrapper.getOriginalData()); 302 } else if (media.isText()) { 303 part.setData(((TextModel) media).getText().getBytes()); 304 } else if (media.isImage() || media.isVideo() || media.isAudio()) { 305 part.setDataUri(media.getUri()); 306 } else { 307 Log.w(TAG, "Unsupport media: " + media); 308 } 309 310 pb.addPart(part); 311 } 312 } 313 314 if (hasForwardLock && isMakingCopy && context != null) { 315 Toast.makeText(context, 316 context.getString(R.string.cannot_forward_drm_obj), 317 Toast.LENGTH_LONG).show(); 318 document = SmilHelper.getDocument(pb); 319 } 320 321 // Create and insert SMIL part(as the first part) into the PduBody. 322 ByteArrayOutputStream out = new ByteArrayOutputStream(); 323 SmilXmlSerializer.serialize(document, out); 324 PduPart smilPart = new PduPart(); 325 smilPart.setContentId("smil".getBytes()); 326 smilPart.setContentLocation("smil.xml".getBytes()); 327 smilPart.setContentType(ContentType.APP_SMIL.getBytes()); 328 smilPart.setData(out.toByteArray()); 329 pb.addPart(0, smilPart); 330 331 return pb; 332 } 333 334 public PduBody makeCopy(Context context) { 335 return makePduBody(context, SmilHelper.getDocument(this), true); 336 } 337 338 public SMILDocument toSmilDocument() { 339 if (mDocumentCache == null) { 340 mDocumentCache = SmilHelper.getDocument(this); 341 } 342 return mDocumentCache; 343 } 344 345 public static PduBody getPduBody(Context context, Uri msg) throws MmsException { 346 PduPersister p = PduPersister.getPduPersister(context); 347 GenericPdu pdu = p.load(msg); 348 349 int msgType = pdu.getMessageType(); 350 if ((msgType == PduHeaders.MESSAGE_TYPE_SEND_REQ) 351 || (msgType == PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF)) { 352 return ((MultimediaMessagePdu) pdu).getBody(); 353 } else { 354 throw new MmsException(); 355 } 356 } 357 358 public void setCurrentMessageSize(int size) { 359 mCurrentMessageSize = size; 360 } 361 362 // getCurrentMessageSize returns the size of the message, not including resizable attachments 363 // such as photos. mCurrentMessageSize is used when adding/deleting/replacing non-resizable 364 // attachments (movies, sounds, etc) in order to compute how much size is left in the message. 365 // The difference between mCurrentMessageSize and the maxSize allowed for a message is then 366 // divided up between the remaining resizable attachments. While this function is public, 367 // it is only used internally between various MMS classes. If the UI wants to know the 368 // size of a MMS message, it should call getTotalMessageSize() instead. 369 public int getCurrentMessageSize() { 370 return mCurrentMessageSize; 371 } 372 373 // getTotalMessageSize returns the total size of the message, including resizable attachments 374 // such as photos. This function is intended to be used by the UI for displaying the size of the 375 // MMS message. 376 public int getTotalMessageSize() { 377 return mTotalMessageSize; 378 } 379 380 public void increaseMessageSize(int increaseSize) { 381 if (increaseSize > 0) { 382 mCurrentMessageSize += increaseSize; 383 } 384 } 385 386 public void decreaseMessageSize(int decreaseSize) { 387 if (decreaseSize > 0) { 388 mCurrentMessageSize -= decreaseSize; 389 } 390 } 391 392 public LayoutModel getLayout() { 393 return mLayout; 394 } 395 396 // 397 // Implement List<E> interface. 398 // 399 public boolean add(SlideModel object) { 400 int increaseSize = object.getSlideSize(); 401 checkMessageSize(increaseSize); 402 403 if ((object != null) && mSlides.add(object)) { 404 increaseMessageSize(increaseSize); 405 object.registerModelChangedObserver(this); 406 for (IModelChangedObserver observer : mModelChangedObservers) { 407 object.registerModelChangedObserver(observer); 408 } 409 notifyModelChanged(true); 410 return true; 411 } 412 return false; 413 } 414 415 public boolean addAll(Collection<? extends SlideModel> collection) { 416 throw new UnsupportedOperationException("Operation not supported."); 417 } 418 419 public void clear() { 420 if (mSlides.size() > 0) { 421 for (SlideModel slide : mSlides) { 422 slide.unregisterModelChangedObserver(this); 423 for (IModelChangedObserver observer : mModelChangedObservers) { 424 slide.unregisterModelChangedObserver(observer); 425 } 426 } 427 mCurrentMessageSize = 0; 428 mSlides.clear(); 429 notifyModelChanged(true); 430 } 431 } 432 433 public boolean contains(Object object) { 434 return mSlides.contains(object); 435 } 436 437 public boolean containsAll(Collection<?> collection) { 438 return mSlides.containsAll(collection); 439 } 440 441 public boolean isEmpty() { 442 return mSlides.isEmpty(); 443 } 444 445 public Iterator<SlideModel> iterator() { 446 return mSlides.iterator(); 447 } 448 449 public boolean remove(Object object) { 450 if ((object != null) && mSlides.remove(object)) { 451 SlideModel slide = (SlideModel) object; 452 decreaseMessageSize(slide.getSlideSize()); 453 slide.unregisterAllModelChangedObservers(); 454 notifyModelChanged(true); 455 return true; 456 } 457 return false; 458 } 459 460 public boolean removeAll(Collection<?> collection) { 461 throw new UnsupportedOperationException("Operation not supported."); 462 } 463 464 public boolean retainAll(Collection<?> collection) { 465 throw new UnsupportedOperationException("Operation not supported."); 466 } 467 468 public int size() { 469 return mSlides.size(); 470 } 471 472 public Object[] toArray() { 473 return mSlides.toArray(); 474 } 475 476 public <T> T[] toArray(T[] array) { 477 return mSlides.toArray(array); 478 } 479 480 public void add(int location, SlideModel object) { 481 if (object != null) { 482 int increaseSize = object.getSlideSize(); 483 checkMessageSize(increaseSize); 484 485 mSlides.add(location, object); 486 increaseMessageSize(increaseSize); 487 object.registerModelChangedObserver(this); 488 for (IModelChangedObserver observer : mModelChangedObservers) { 489 object.registerModelChangedObserver(observer); 490 } 491 notifyModelChanged(true); 492 } 493 } 494 495 public boolean addAll(int location, 496 Collection<? extends SlideModel> collection) { 497 throw new UnsupportedOperationException("Operation not supported."); 498 } 499 500 public SlideModel get(int location) { 501 return (location >= 0 && location < mSlides.size()) ? mSlides.get(location) : null; 502 } 503 504 public int indexOf(Object object) { 505 return mSlides.indexOf(object); 506 } 507 508 public int lastIndexOf(Object object) { 509 return mSlides.lastIndexOf(object); 510 } 511 512 public ListIterator<SlideModel> listIterator() { 513 return mSlides.listIterator(); 514 } 515 516 public ListIterator<SlideModel> listIterator(int location) { 517 return mSlides.listIterator(location); 518 } 519 520 public SlideModel remove(int location) { 521 SlideModel slide = mSlides.remove(location); 522 if (slide != null) { 523 decreaseMessageSize(slide.getSlideSize()); 524 slide.unregisterAllModelChangedObservers(); 525 notifyModelChanged(true); 526 } 527 return slide; 528 } 529 530 public SlideModel set(int location, SlideModel object) { 531 SlideModel slide = mSlides.get(location); 532 if (null != object) { 533 int removeSize = 0; 534 int addSize = object.getSlideSize(); 535 if (null != slide) { 536 removeSize = slide.getSlideSize(); 537 } 538 if (addSize > removeSize) { 539 checkMessageSize(addSize - removeSize); 540 increaseMessageSize(addSize - removeSize); 541 } else { 542 decreaseMessageSize(removeSize - addSize); 543 } 544 } 545 546 slide = mSlides.set(location, object); 547 if (slide != null) { 548 slide.unregisterAllModelChangedObservers(); 549 } 550 551 if (object != null) { 552 object.registerModelChangedObserver(this); 553 for (IModelChangedObserver observer : mModelChangedObservers) { 554 object.registerModelChangedObserver(observer); 555 } 556 } 557 558 notifyModelChanged(true); 559 return slide; 560 } 561 562 public List<SlideModel> subList(int start, int end) { 563 return mSlides.subList(start, end); 564 } 565 566 @Override 567 protected void registerModelChangedObserverInDescendants( 568 IModelChangedObserver observer) { 569 mLayout.registerModelChangedObserver(observer); 570 571 for (SlideModel slide : mSlides) { 572 slide.registerModelChangedObserver(observer); 573 } 574 } 575 576 @Override 577 protected void unregisterModelChangedObserverInDescendants( 578 IModelChangedObserver observer) { 579 mLayout.unregisterModelChangedObserver(observer); 580 581 for (SlideModel slide : mSlides) { 582 slide.unregisterModelChangedObserver(observer); 583 } 584 } 585 586 @Override 587 protected void unregisterAllModelChangedObserversInDescendants() { 588 mLayout.unregisterAllModelChangedObservers(); 589 590 for (SlideModel slide : mSlides) { 591 slide.unregisterAllModelChangedObservers(); 592 } 593 } 594 595 public void onModelChanged(Model model, boolean dataChanged) { 596 if (dataChanged) { 597 mDocumentCache = null; 598 mPduBodyCache = null; 599 } 600 } 601 602 public void sync(PduBody pb) { 603 for (SlideModel slide : mSlides) { 604 for (MediaModel media : slide) { 605 PduPart part = pb.getPartByContentLocation(media.getSrc()); 606 if (part != null) { 607 media.setUri(part.getDataUri()); 608 } 609 } 610 } 611 } 612 613 public void checkMessageSize(int increaseSize) throws ContentRestrictionException { 614 ContentRestriction cr = ContentRestrictionFactory.getContentRestriction(); 615 cr.checkMessageSize(mCurrentMessageSize, increaseSize, mContext.getContentResolver()); 616 } 617 618 /** 619 * Determines whether this is a "simple" slideshow. 620 * Criteria: 621 * - Exactly one slide 622 * - Exactly one multimedia attachment, but no audio 623 * - It can optionally have a caption 624 */ 625 public boolean isSimple() { 626 // There must be one (and only one) slide. 627 if (size() != 1) 628 return false; 629 630 SlideModel slide = get(0); 631 // The slide must have either an image or video, but not both. 632 if (!(slide.hasImage() ^ slide.hasVideo())) 633 return false; 634 635 // No audio allowed. 636 if (slide.hasAudio()) 637 return false; 638 639 return true; 640 } 641 642 /** 643 * Make sure the text in slide 0 is no longer holding onto a reference to the text 644 * in the message text box. 645 */ 646 public void prepareForSend() { 647 if (size() == 1) { 648 TextModel text = get(0).getText(); 649 if (text != null) { 650 text.cloneText(); 651 } 652 } 653 } 654 655 /** 656 * Resize all the resizeable media objects to fit in the remaining size of the slideshow. 657 * This should be called off of the UI thread. 658 * 659 * @throws MmsException, ExceedMessageSizeException 660 */ 661 public void finalResize(Uri messageUri) throws MmsException, ExceedMessageSizeException { 662 663 // Figure out if we have any media items that need to be resized and total up the 664 // sizes of the items that can't be resized. 665 int resizableCnt = 0; 666 int fixedSizeTotal = 0; 667 for (SlideModel slide : mSlides) { 668 for (MediaModel media : slide) { 669 if (media.getMediaResizable()) { 670 ++resizableCnt; 671 } else { 672 fixedSizeTotal += media.getMediaSize(); 673 } 674 } 675 } 676 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 677 Log.v(TAG, "finalResize: original message size: " + getCurrentMessageSize() + 678 " getMaxMessageSize: " + MmsConfig.getMaxMessageSize() + 679 " fixedSizeTotal: " + fixedSizeTotal); 680 } 681 if (resizableCnt > 0) { 682 int remainingSize = MmsConfig.getMaxMessageSize() - fixedSizeTotal - SLIDESHOW_SLOP; 683 if (remainingSize <= 0) { 684 throw new ExceedMessageSizeException("No room for pictures"); 685 } 686 long messageId = ContentUris.parseId(messageUri); 687 int bytesPerMediaItem = remainingSize / resizableCnt; 688 // Resize the resizable media items to fit within their byte limit. 689 for (SlideModel slide : mSlides) { 690 for (MediaModel media : slide) { 691 if (media.getMediaResizable()) { 692 media.resizeMedia(bytesPerMediaItem, messageId); 693 } 694 } 695 } 696 // One last time through to calc the real message size. 697 int totalSize = 0; 698 for (SlideModel slide : mSlides) { 699 for (MediaModel media : slide) { 700 totalSize += media.getMediaSize(); 701 } 702 } 703 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 704 Log.v(TAG, "finalResize: new message size: " + totalSize); 705 } 706 707 if (totalSize > MmsConfig.getMaxMessageSize()) { 708 throw new ExceedMessageSizeException("After compressing pictures, message too big"); 709 } 710 setCurrentMessageSize(totalSize); 711 712 onModelChanged(this, true); // clear the cached pdu body 713 PduBody pb = toPduBody(); 714 // This will write out all the new parts to: 715 // /data/data/com.android.providers.telephony/app_parts 716 // and at the same time delete the old parts. 717 PduPersister.getPduPersister(mContext).updateParts(messageUri, pb); 718 } 719 } 720 721 } 722