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