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