SlideshowModel.java revision 469db72fcf219fd4c60bc9b01c728bdc3e866d40
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 java.io.ByteArrayOutputStream;
22import java.io.FileNotFoundException;
23import java.io.IOException;
24import java.io.InputStream;
25import java.util.ArrayList;
26import java.util.Collection;
27import java.util.HashMap;
28import java.util.Iterator;
29import java.util.List;
30import java.util.ListIterator;
31
32import org.w3c.dom.NodeList;
33import org.w3c.dom.events.EventTarget;
34import org.w3c.dom.smil.SMILDocument;
35import org.w3c.dom.smil.SMILElement;
36import org.w3c.dom.smil.SMILLayoutElement;
37import org.w3c.dom.smil.SMILMediaElement;
38import org.w3c.dom.smil.SMILParElement;
39import org.w3c.dom.smil.SMILRegionElement;
40import org.w3c.dom.smil.SMILRootLayoutElement;
41
42import android.content.ContentResolver;
43import android.content.ContentUris;
44import android.content.Context;
45import android.net.Uri;
46import android.text.TextUtils;
47import android.util.Log;
48
49import com.android.mms.ContentRestrictionException;
50import com.android.mms.ExceedMessageSizeException;
51import com.android.mms.LogTag;
52import com.android.mms.MmsConfig;
53import com.android.mms.dom.smil.parser.SmilXmlSerializer;
54import com.android.mms.layout.LayoutManager;
55import com.google.android.mms.ContentType;
56import com.google.android.mms.MmsException;
57import com.google.android.mms.pdu.GenericPdu;
58import com.google.android.mms.pdu.MultimediaMessagePdu;
59import com.google.android.mms.pdu.PduBody;
60import com.google.android.mms.pdu.PduHeaders;
61import com.google.android.mms.pdu.PduPart;
62import com.google.android.mms.pdu.PduPersister;
63
64public 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