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