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