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