| /* |
| * Copyright (C) 2008 Esmertec AG. |
| * Copyright (C) 2008 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.mms.model; |
| |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.ListIterator; |
| |
| import org.w3c.dom.NodeList; |
| import org.w3c.dom.events.EventTarget; |
| import org.w3c.dom.smil.SMILDocument; |
| import org.w3c.dom.smil.SMILElement; |
| import org.w3c.dom.smil.SMILLayoutElement; |
| import org.w3c.dom.smil.SMILMediaElement; |
| import org.w3c.dom.smil.SMILParElement; |
| import org.w3c.dom.smil.SMILRegionElement; |
| import org.w3c.dom.smil.SMILRootLayoutElement; |
| |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.Context; |
| import android.net.Uri; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import com.android.mms.ContentRestrictionException; |
| import com.android.mms.ExceedMessageSizeException; |
| import com.android.mms.LogTag; |
| import com.android.mms.MmsConfig; |
| import com.android.mms.dom.smil.parser.SmilXmlSerializer; |
| import com.android.mms.layout.LayoutManager; |
| import com.google.android.mms.ContentType; |
| import com.google.android.mms.MmsException; |
| import com.google.android.mms.pdu.GenericPdu; |
| import com.google.android.mms.pdu.MultimediaMessagePdu; |
| import com.google.android.mms.pdu.PduBody; |
| import com.google.android.mms.pdu.PduHeaders; |
| import com.google.android.mms.pdu.PduPart; |
| import com.google.android.mms.pdu.PduPersister; |
| |
| public class SlideshowModel extends Model |
| implements List<SlideModel>, IModelChangedObserver { |
| private static final String TAG = "Mms/slideshow"; |
| |
| private final LayoutModel mLayout; |
| private final ArrayList<SlideModel> mSlides; |
| private SMILDocument mDocumentCache; |
| private PduBody mPduBodyCache; |
| private int mCurrentMessageSize; // This is the current message size, not including |
| // attachments that can be resized (such as photos) |
| private int mTotalMessageSize; // This is the computed total message size |
| private Context mContext; |
| |
| // amount of space to leave in a slideshow for text and overhead. |
| public static final int SLIDESHOW_SLOP = 1024; |
| |
| private SlideshowModel(Context context) { |
| mLayout = new LayoutModel(); |
| mSlides = new ArrayList<SlideModel>(); |
| mContext = context; |
| } |
| |
| private SlideshowModel ( |
| LayoutModel layouts, ArrayList<SlideModel> slides, |
| SMILDocument documentCache, PduBody pbCache, |
| Context context) { |
| mLayout = layouts; |
| mSlides = slides; |
| mContext = context; |
| |
| mDocumentCache = documentCache; |
| mPduBodyCache = pbCache; |
| for (SlideModel slide : mSlides) { |
| increaseMessageSize(slide.getSlideSize()); |
| slide.setParent(this); |
| } |
| } |
| |
| public static SlideshowModel createNew(Context context) { |
| return new SlideshowModel(context); |
| } |
| |
| public static SlideshowModel createFromMessageUri( |
| Context context, Uri uri) throws MmsException { |
| return createFromPduBody(context, getPduBody(context, uri)); |
| } |
| |
| public static SlideshowModel createFromPduBody(Context context, PduBody pb) throws MmsException { |
| SMILDocument document = SmilHelper.getDocument(pb); |
| |
| // Create root-layout model. |
| SMILLayoutElement sle = document.getLayout(); |
| SMILRootLayoutElement srle = sle.getRootLayout(); |
| int w = srle.getWidth(); |
| int h = srle.getHeight(); |
| if ((w == 0) || (h == 0)) { |
| w = LayoutManager.getInstance().getLayoutParameters().getWidth(); |
| h = LayoutManager.getInstance().getLayoutParameters().getHeight(); |
| srle.setWidth(w); |
| srle.setHeight(h); |
| } |
| RegionModel rootLayout = new RegionModel( |
| null, 0, 0, w, h); |
| |
| // Create region models. |
| ArrayList<RegionModel> regions = new ArrayList<RegionModel>(); |
| NodeList nlRegions = sle.getRegions(); |
| int regionsNum = nlRegions.getLength(); |
| |
| for (int i = 0; i < regionsNum; i++) { |
| SMILRegionElement sre = (SMILRegionElement) nlRegions.item(i); |
| RegionModel r = new RegionModel(sre.getId(), sre.getFit(), |
| sre.getLeft(), sre.getTop(), sre.getWidth(), sre.getHeight(), |
| sre.getBackgroundColor()); |
| regions.add(r); |
| } |
| LayoutModel layouts = new LayoutModel(rootLayout, regions); |
| |
| // Create slide models. |
| SMILElement docBody = document.getBody(); |
| NodeList slideNodes = docBody.getChildNodes(); |
| int slidesNum = slideNodes.getLength(); |
| ArrayList<SlideModel> slides = new ArrayList<SlideModel>(slidesNum); |
| int totalMessageSize = 0; |
| |
| for (int i = 0; i < slidesNum; i++) { |
| // FIXME: This is NOT compatible with the SMILDocument which is |
| // generated by some other mobile phones. |
| SMILParElement par = (SMILParElement) slideNodes.item(i); |
| |
| // Create media models for each slide. |
| NodeList mediaNodes = par.getChildNodes(); |
| int mediaNum = mediaNodes.getLength(); |
| ArrayList<MediaModel> mediaSet = new ArrayList<MediaModel>(mediaNum); |
| |
| for (int j = 0; j < mediaNum; j++) { |
| SMILMediaElement sme = (SMILMediaElement) mediaNodes.item(j); |
| try { |
| MediaModel media = MediaModelFactory.getMediaModel( |
| context, sme, layouts, pb); |
| |
| /* |
| * This is for slide duration value set. |
| * If mms server does not support slide duration. |
| */ |
| if (!MmsConfig.getSlideDurationEnabled()) { |
| int mediadur = media.getDuration(); |
| float dur = par.getDur(); |
| if (dur == 0) { |
| mediadur = MmsConfig.getMinimumSlideElementDuration() * 1000; |
| media.setDuration(mediadur); |
| } |
| |
| if ((int)mediadur / 1000 != dur) { |
| String tag = sme.getTagName(); |
| |
| if (ContentType.isVideoType(media.mContentType) |
| || tag.equals(SmilHelper.ELEMENT_TAG_VIDEO) |
| || ContentType.isAudioType(media.mContentType) |
| || tag.equals(SmilHelper.ELEMENT_TAG_AUDIO)) { |
| /* |
| * add 1 sec to release and close audio/video |
| * for guaranteeing the audio/video playing. |
| * because the mmsc does not support the slide duration. |
| */ |
| par.setDur((float)mediadur / 1000 + 1); |
| } else { |
| /* |
| * If a slide has an image and an audio/video element |
| * and the audio/video element has longer duration than the image, |
| * The Image disappear before the slide play done. so have to match |
| * an image duration to the slide duration. |
| */ |
| if ((int)mediadur / 1000 < dur) { |
| media.setDuration((int)dur * 1000); |
| } else { |
| if ((int)dur != 0) { |
| media.setDuration((int)dur * 1000); |
| } else { |
| par.setDur((float)mediadur / 1000); |
| } |
| } |
| } |
| } |
| } |
| SmilHelper.addMediaElementEventListeners( |
| (EventTarget) sme, media); |
| mediaSet.add(media); |
| totalMessageSize += media.getMediaSize(); |
| } catch (IOException e) { |
| Log.e(TAG, e.getMessage(), e); |
| } catch (IllegalArgumentException e) { |
| Log.e(TAG, e.getMessage(), e); |
| } |
| } |
| |
| SlideModel slide = new SlideModel((int) (par.getDur() * 1000), mediaSet); |
| slide.setFill(par.getFill()); |
| SmilHelper.addParElementEventListeners((EventTarget) par, slide); |
| slides.add(slide); |
| } |
| |
| SlideshowModel slideshow = new SlideshowModel(layouts, slides, document, pb, context); |
| slideshow.mTotalMessageSize = totalMessageSize; |
| slideshow.registerModelChangedObserver(slideshow); |
| return slideshow; |
| } |
| |
| public PduBody toPduBody() { |
| if (mPduBodyCache == null) { |
| mDocumentCache = SmilHelper.getDocument(this); |
| mPduBodyCache = makePduBody(mDocumentCache); |
| } |
| return mPduBodyCache; |
| } |
| |
| private PduBody makePduBody(SMILDocument document) { |
| PduBody pb = new PduBody(); |
| |
| boolean hasForwardLock = false; |
| for (SlideModel slide : mSlides) { |
| for (MediaModel media : slide) { |
| PduPart part = new PduPart(); |
| |
| if (media.isText()) { |
| TextModel text = (TextModel) media; |
| // Don't create empty text part. |
| if (TextUtils.isEmpty(text.getText())) { |
| continue; |
| } |
| // Set Charset if it's a text media. |
| part.setCharset(text.getCharset()); |
| } |
| |
| // Set Content-Type. |
| part.setContentType(media.getContentType().getBytes()); |
| |
| String src = media.getSrc(); |
| String location; |
| boolean startWithContentId = src.startsWith("cid:"); |
| if (startWithContentId) { |
| location = src.substring("cid:".length()); |
| } else { |
| location = src; |
| } |
| |
| // Set Content-Location. |
| part.setContentLocation(location.getBytes()); |
| |
| // Set Content-Id. |
| if (startWithContentId) { |
| //Keep the original Content-Id. |
| part.setContentId(location.getBytes()); |
| } |
| else { |
| int index = location.lastIndexOf("."); |
| String contentId = (index == -1) ? location |
| : location.substring(0, index); |
| part.setContentId(contentId.getBytes()); |
| } |
| |
| if (media.isText()) { |
| part.setData(((TextModel) media).getText().getBytes()); |
| } else if (media.isImage() || media.isVideo() || media.isAudio()) { |
| part.setDataUri(media.getUri()); |
| } else { |
| Log.w(TAG, "Unsupport media: " + media); |
| } |
| |
| pb.addPart(part); |
| } |
| } |
| |
| // Create and insert SMIL part(as the first part) into the PduBody. |
| ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| SmilXmlSerializer.serialize(document, out); |
| PduPart smilPart = new PduPart(); |
| smilPart.setContentId("smil".getBytes()); |
| smilPart.setContentLocation("smil.xml".getBytes()); |
| smilPart.setContentType(ContentType.APP_SMIL.getBytes()); |
| smilPart.setData(out.toByteArray()); |
| pb.addPart(0, smilPart); |
| |
| return pb; |
| } |
| |
| public HashMap<Uri, InputStream> openPartFiles(ContentResolver cr) { |
| HashMap<Uri, InputStream> openedFiles = null; // Don't create unless we have to |
| |
| for (SlideModel slide : mSlides) { |
| for (MediaModel media : slide) { |
| if (media.isText()) { |
| continue; |
| } |
| Uri uri = media.getUri(); |
| InputStream is; |
| try { |
| is = cr.openInputStream(uri); |
| if (is != null) { |
| if (openedFiles == null) { |
| openedFiles = new HashMap<Uri, InputStream>(); |
| } |
| openedFiles.put(uri, is); |
| } |
| } catch (FileNotFoundException e) { |
| Log.e(TAG, "openPartFiles couldn't open: " + uri, e); |
| } |
| } |
| } |
| return openedFiles; |
| } |
| |
| public PduBody makeCopy() { |
| return makePduBody(SmilHelper.getDocument(this)); |
| } |
| |
| public SMILDocument toSmilDocument() { |
| if (mDocumentCache == null) { |
| mDocumentCache = SmilHelper.getDocument(this); |
| } |
| return mDocumentCache; |
| } |
| |
| public static PduBody getPduBody(Context context, Uri msg) throws MmsException { |
| PduPersister p = PduPersister.getPduPersister(context); |
| GenericPdu pdu = p.load(msg); |
| |
| int msgType = pdu.getMessageType(); |
| if ((msgType == PduHeaders.MESSAGE_TYPE_SEND_REQ) |
| || (msgType == PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF)) { |
| return ((MultimediaMessagePdu) pdu).getBody(); |
| } else { |
| throw new MmsException(); |
| } |
| } |
| |
| public void setCurrentMessageSize(int size) { |
| mCurrentMessageSize = size; |
| } |
| |
| // getCurrentMessageSize returns the size of the message, not including resizable attachments |
| // such as photos. mCurrentMessageSize is used when adding/deleting/replacing non-resizable |
| // attachments (movies, sounds, etc) in order to compute how much size is left in the message. |
| // The difference between mCurrentMessageSize and the maxSize allowed for a message is then |
| // divided up between the remaining resizable attachments. While this function is public, |
| // it is only used internally between various MMS classes. If the UI wants to know the |
| // size of a MMS message, it should call getTotalMessageSize() instead. |
| public int getCurrentMessageSize() { |
| return mCurrentMessageSize; |
| } |
| |
| // getTotalMessageSize returns the total size of the message, including resizable attachments |
| // such as photos. This function is intended to be used by the UI for displaying the size of the |
| // MMS message. |
| public int getTotalMessageSize() { |
| return mTotalMessageSize; |
| } |
| |
| public void increaseMessageSize(int increaseSize) { |
| if (increaseSize > 0) { |
| mCurrentMessageSize += increaseSize; |
| } |
| } |
| |
| public void decreaseMessageSize(int decreaseSize) { |
| if (decreaseSize > 0) { |
| mCurrentMessageSize -= decreaseSize; |
| } |
| } |
| |
| public LayoutModel getLayout() { |
| return mLayout; |
| } |
| |
| // |
| // Implement List<E> interface. |
| // |
| public boolean add(SlideModel object) { |
| int increaseSize = object.getSlideSize(); |
| checkMessageSize(increaseSize); |
| |
| if ((object != null) && mSlides.add(object)) { |
| increaseMessageSize(increaseSize); |
| object.registerModelChangedObserver(this); |
| for (IModelChangedObserver observer : mModelChangedObservers) { |
| object.registerModelChangedObserver(observer); |
| } |
| notifyModelChanged(true); |
| return true; |
| } |
| return false; |
| } |
| |
| public boolean addAll(Collection<? extends SlideModel> collection) { |
| throw new UnsupportedOperationException("Operation not supported."); |
| } |
| |
| public void clear() { |
| if (mSlides.size() > 0) { |
| for (SlideModel slide : mSlides) { |
| slide.unregisterModelChangedObserver(this); |
| for (IModelChangedObserver observer : mModelChangedObservers) { |
| slide.unregisterModelChangedObserver(observer); |
| } |
| } |
| mCurrentMessageSize = 0; |
| mSlides.clear(); |
| notifyModelChanged(true); |
| } |
| } |
| |
| public boolean contains(Object object) { |
| return mSlides.contains(object); |
| } |
| |
| public boolean containsAll(Collection<?> collection) { |
| return mSlides.containsAll(collection); |
| } |
| |
| public boolean isEmpty() { |
| return mSlides.isEmpty(); |
| } |
| |
| public Iterator<SlideModel> iterator() { |
| return mSlides.iterator(); |
| } |
| |
| public boolean remove(Object object) { |
| if ((object != null) && mSlides.remove(object)) { |
| SlideModel slide = (SlideModel) object; |
| decreaseMessageSize(slide.getSlideSize()); |
| slide.unregisterAllModelChangedObservers(); |
| notifyModelChanged(true); |
| return true; |
| } |
| return false; |
| } |
| |
| public boolean removeAll(Collection<?> collection) { |
| throw new UnsupportedOperationException("Operation not supported."); |
| } |
| |
| public boolean retainAll(Collection<?> collection) { |
| throw new UnsupportedOperationException("Operation not supported."); |
| } |
| |
| public int size() { |
| return mSlides.size(); |
| } |
| |
| public Object[] toArray() { |
| return mSlides.toArray(); |
| } |
| |
| public <T> T[] toArray(T[] array) { |
| return mSlides.toArray(array); |
| } |
| |
| public void add(int location, SlideModel object) { |
| if (object != null) { |
| int increaseSize = object.getSlideSize(); |
| checkMessageSize(increaseSize); |
| |
| mSlides.add(location, object); |
| increaseMessageSize(increaseSize); |
| object.registerModelChangedObserver(this); |
| for (IModelChangedObserver observer : mModelChangedObservers) { |
| object.registerModelChangedObserver(observer); |
| } |
| notifyModelChanged(true); |
| } |
| } |
| |
| public boolean addAll(int location, |
| Collection<? extends SlideModel> collection) { |
| throw new UnsupportedOperationException("Operation not supported."); |
| } |
| |
| public SlideModel get(int location) { |
| return (location >= 0 && location < mSlides.size()) ? mSlides.get(location) : null; |
| } |
| |
| public int indexOf(Object object) { |
| return mSlides.indexOf(object); |
| } |
| |
| public int lastIndexOf(Object object) { |
| return mSlides.lastIndexOf(object); |
| } |
| |
| public ListIterator<SlideModel> listIterator() { |
| return mSlides.listIterator(); |
| } |
| |
| public ListIterator<SlideModel> listIterator(int location) { |
| return mSlides.listIterator(location); |
| } |
| |
| public SlideModel remove(int location) { |
| SlideModel slide = mSlides.remove(location); |
| if (slide != null) { |
| decreaseMessageSize(slide.getSlideSize()); |
| slide.unregisterAllModelChangedObservers(); |
| notifyModelChanged(true); |
| } |
| return slide; |
| } |
| |
| public SlideModel set(int location, SlideModel object) { |
| SlideModel slide = mSlides.get(location); |
| if (null != object) { |
| int removeSize = 0; |
| int addSize = object.getSlideSize(); |
| if (null != slide) { |
| removeSize = slide.getSlideSize(); |
| } |
| if (addSize > removeSize) { |
| checkMessageSize(addSize - removeSize); |
| increaseMessageSize(addSize - removeSize); |
| } else { |
| decreaseMessageSize(removeSize - addSize); |
| } |
| } |
| |
| slide = mSlides.set(location, object); |
| if (slide != null) { |
| slide.unregisterAllModelChangedObservers(); |
| } |
| |
| if (object != null) { |
| object.registerModelChangedObserver(this); |
| for (IModelChangedObserver observer : mModelChangedObservers) { |
| object.registerModelChangedObserver(observer); |
| } |
| } |
| |
| notifyModelChanged(true); |
| return slide; |
| } |
| |
| public List<SlideModel> subList(int start, int end) { |
| return mSlides.subList(start, end); |
| } |
| |
| @Override |
| protected void registerModelChangedObserverInDescendants( |
| IModelChangedObserver observer) { |
| mLayout.registerModelChangedObserver(observer); |
| |
| for (SlideModel slide : mSlides) { |
| slide.registerModelChangedObserver(observer); |
| } |
| } |
| |
| @Override |
| protected void unregisterModelChangedObserverInDescendants( |
| IModelChangedObserver observer) { |
| mLayout.unregisterModelChangedObserver(observer); |
| |
| for (SlideModel slide : mSlides) { |
| slide.unregisterModelChangedObserver(observer); |
| } |
| } |
| |
| @Override |
| protected void unregisterAllModelChangedObserversInDescendants() { |
| mLayout.unregisterAllModelChangedObservers(); |
| |
| for (SlideModel slide : mSlides) { |
| slide.unregisterAllModelChangedObservers(); |
| } |
| } |
| |
| public void onModelChanged(Model model, boolean dataChanged) { |
| if (dataChanged) { |
| mDocumentCache = null; |
| mPduBodyCache = null; |
| } |
| } |
| |
| public void sync(PduBody pb) { |
| for (SlideModel slide : mSlides) { |
| for (MediaModel media : slide) { |
| PduPart part = pb.getPartByContentLocation(media.getSrc()); |
| if (part != null) { |
| media.setUri(part.getDataUri()); |
| } |
| } |
| } |
| } |
| |
| public void checkMessageSize(int increaseSize) throws ContentRestrictionException { |
| ContentRestriction cr = ContentRestrictionFactory.getContentRestriction(); |
| cr.checkMessageSize(mCurrentMessageSize, increaseSize, mContext.getContentResolver()); |
| } |
| |
| /** |
| * Determines whether this is a "simple" slideshow. |
| * Criteria: |
| * - Exactly one slide |
| * - Exactly one multimedia attachment, but no audio |
| * - It can optionally have a caption |
| */ |
| public boolean isSimple() { |
| // There must be one (and only one) slide. |
| if (size() != 1) |
| return false; |
| |
| SlideModel slide = get(0); |
| // The slide must have either an image or video, but not both. |
| if (!(slide.hasImage() ^ slide.hasVideo())) |
| return false; |
| |
| // No audio allowed. |
| if (slide.hasAudio()) |
| return false; |
| |
| return true; |
| } |
| |
| /** |
| * Make sure the text in slide 0 is no longer holding onto a reference to the text |
| * in the message text box. |
| */ |
| public void prepareForSend() { |
| if (size() == 1) { |
| TextModel text = get(0).getText(); |
| if (text != null) { |
| text.cloneText(); |
| } |
| } |
| } |
| |
| /** |
| * Resize all the resizeable media objects to fit in the remaining size of the slideshow. |
| * This should be called off of the UI thread. |
| * |
| * @throws MmsException, ExceedMessageSizeException |
| */ |
| public void finalResize(Uri messageUri) throws MmsException, ExceedMessageSizeException { |
| |
| // Figure out if we have any media items that need to be resized and total up the |
| // sizes of the items that can't be resized. |
| int resizableCnt = 0; |
| int fixedSizeTotal = 0; |
| for (SlideModel slide : mSlides) { |
| for (MediaModel media : slide) { |
| if (media.getMediaResizable()) { |
| ++resizableCnt; |
| } else { |
| fixedSizeTotal += media.getMediaSize(); |
| } |
| } |
| } |
| if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { |
| Log.v(TAG, "finalResize: original message size: " + getCurrentMessageSize() + |
| " getMaxMessageSize: " + MmsConfig.getMaxMessageSize() + |
| " fixedSizeTotal: " + fixedSizeTotal); |
| } |
| if (resizableCnt > 0) { |
| int remainingSize = MmsConfig.getMaxMessageSize() - fixedSizeTotal - SLIDESHOW_SLOP; |
| if (remainingSize <= 0) { |
| throw new ExceedMessageSizeException("No room for pictures"); |
| } |
| long messageId = ContentUris.parseId(messageUri); |
| int bytesPerMediaItem = remainingSize / resizableCnt; |
| // Resize the resizable media items to fit within their byte limit. |
| for (SlideModel slide : mSlides) { |
| for (MediaModel media : slide) { |
| if (media.getMediaResizable()) { |
| media.resizeMedia(bytesPerMediaItem, messageId); |
| } |
| } |
| } |
| // One last time through to calc the real message size. |
| int totalSize = 0; |
| for (SlideModel slide : mSlides) { |
| for (MediaModel media : slide) { |
| totalSize += media.getMediaSize(); |
| } |
| } |
| if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { |
| Log.v(TAG, "finalResize: new message size: " + totalSize); |
| } |
| |
| if (totalSize > MmsConfig.getMaxMessageSize()) { |
| throw new ExceedMessageSizeException("After compressing pictures, message too big"); |
| } |
| setCurrentMessageSize(totalSize); |
| |
| onModelChanged(this, true); // clear the cached pdu body |
| PduBody pb = toPduBody(); |
| // This will write out all the new parts to: |
| // /data/data/com.android.providers.telephony/app_parts |
| // and at the same time delete the old parts. |
| PduPersister.getPduPersister(mContext).updateParts(messageUri, pb, null); |
| } |
| } |
| |
| } |