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 android.drm.mobile1.DrmException;
28import com.android.mms.drm.DrmWrapper;
29import com.android.mms.layout.LayoutManager;
30import com.google.android.mms.ContentType;
31import com.google.android.mms.MmsException;
32import com.google.android.mms.pdu.GenericPdu;
33import com.google.android.mms.pdu.MultimediaMessagePdu;
34import com.google.android.mms.pdu.PduBody;
35import com.google.android.mms.pdu.PduHeaders;
36import com.google.android.mms.pdu.PduPart;
37import com.google.android.mms.pdu.PduPersister;
38
39import org.w3c.dom.NodeList;
40import org.w3c.dom.events.EventTarget;
41import org.w3c.dom.smil.SMILDocument;
42import org.w3c.dom.smil.SMILElement;
43import org.w3c.dom.smil.SMILLayoutElement;
44import org.w3c.dom.smil.SMILMediaElement;
45import org.w3c.dom.smil.SMILParElement;
46import org.w3c.dom.smil.SMILRegionElement;
47import org.w3c.dom.smil.SMILRootLayoutElement;
48
49import android.content.ContentUris;
50import android.content.Context;
51import android.net.Uri;
52import android.text.TextUtils;
53import android.util.Log;
54import android.widget.Toast;
55
56import java.io.ByteArrayOutputStream;
57import java.io.IOException;
58import java.util.ArrayList;
59import java.util.Collection;
60import java.util.Iterator;
61import java.util.List;
62import java.util.ListIterator;
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 (DrmException e) {
214                    Log.e(TAG, e.getMessage(), e);
215                } catch (IOException e) {
216                    Log.e(TAG, e.getMessage(), e);
217                } catch (IllegalArgumentException e) {
218                    Log.e(TAG, e.getMessage(), e);
219                }
220            }
221
222            SlideModel slide = new SlideModel((int) (par.getDur() * 1000), mediaSet);
223            slide.setFill(par.getFill());
224            SmilHelper.addParElementEventListeners((EventTarget) par, slide);
225            slides.add(slide);
226        }
227
228        SlideshowModel slideshow = new SlideshowModel(layouts, slides, document, pb, context);
229        slideshow.mTotalMessageSize = totalMessageSize;
230        slideshow.registerModelChangedObserver(slideshow);
231        return slideshow;
232    }
233
234    public PduBody toPduBody() {
235        if (mPduBodyCache == null) {
236            mDocumentCache = SmilHelper.getDocument(this);
237            mPduBodyCache = makePduBody(mDocumentCache);
238        }
239        return mPduBodyCache;
240    }
241
242    private PduBody makePduBody(SMILDocument document) {
243        return makePduBody(null, document, false);
244    }
245
246    private PduBody makePduBody(Context context, SMILDocument document, boolean isMakingCopy) {
247        PduBody pb = new PduBody();
248
249        boolean hasForwardLock = false;
250        for (SlideModel slide : mSlides) {
251            for (MediaModel media : slide) {
252                if (isMakingCopy) {
253                    if (media.isDrmProtected() && !media.isAllowedToForward()) {
254                        hasForwardLock = true;
255                        continue;
256                    }
257                }
258
259                PduPart part = new PduPart();
260
261                if (media.isText()) {
262                    TextModel text = (TextModel) media;
263                    // Don't create empty text part.
264                    if (TextUtils.isEmpty(text.getText())) {
265                        continue;
266                    }
267                    // Set Charset if it's a text media.
268                    part.setCharset(text.getCharset());
269                }
270
271                // Set Content-Type.
272                part.setContentType(media.getContentType().getBytes());
273
274                String src = media.getSrc();
275                String location;
276                boolean startWithContentId = src.startsWith("cid:");
277                if (startWithContentId) {
278                    location = src.substring("cid:".length());
279                } else {
280                    location = src;
281                }
282
283                // Set Content-Location.
284                part.setContentLocation(location.getBytes());
285
286                // Set Content-Id.
287                if (startWithContentId) {
288                    //Keep the original Content-Id.
289                    part.setContentId(location.getBytes());
290                }
291                else {
292                    int index = location.lastIndexOf(".");
293                    String contentId = (index == -1) ? location
294                            : location.substring(0, index);
295                    part.setContentId(contentId.getBytes());
296                }
297
298                if (media.isDrmProtected()) {
299                    DrmWrapper wrapper = media.getDrmObject();
300                    part.setDataUri(wrapper.getOriginalUri());
301                    part.setData(wrapper.getOriginalData());
302                } else if (media.isText()) {
303                    part.setData(((TextModel) media).getText().getBytes());
304                } else if (media.isImage() || media.isVideo() || media.isAudio()) {
305                    part.setDataUri(media.getUri());
306                } else {
307                    Log.w(TAG, "Unsupport media: " + media);
308                }
309
310                pb.addPart(part);
311            }
312        }
313
314        if (hasForwardLock && isMakingCopy && context != null) {
315            Toast.makeText(context,
316                    context.getString(R.string.cannot_forward_drm_obj),
317                    Toast.LENGTH_LONG).show();
318            document = SmilHelper.getDocument(pb);
319        }
320
321        // Create and insert SMIL part(as the first part) into the PduBody.
322        ByteArrayOutputStream out = new ByteArrayOutputStream();
323        SmilXmlSerializer.serialize(document, out);
324        PduPart smilPart = new PduPart();
325        smilPart.setContentId("smil".getBytes());
326        smilPart.setContentLocation("smil.xml".getBytes());
327        smilPart.setContentType(ContentType.APP_SMIL.getBytes());
328        smilPart.setData(out.toByteArray());
329        pb.addPart(0, smilPart);
330
331        return pb;
332    }
333
334    public PduBody makeCopy(Context context) {
335        return makePduBody(context, SmilHelper.getDocument(this), true);
336    }
337
338    public SMILDocument toSmilDocument() {
339        if (mDocumentCache == null) {
340            mDocumentCache = SmilHelper.getDocument(this);
341        }
342        return mDocumentCache;
343    }
344
345    public static PduBody getPduBody(Context context, Uri msg) throws MmsException {
346        PduPersister p = PduPersister.getPduPersister(context);
347        GenericPdu pdu = p.load(msg);
348
349        int msgType = pdu.getMessageType();
350        if ((msgType == PduHeaders.MESSAGE_TYPE_SEND_REQ)
351                || (msgType == PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF)) {
352            return ((MultimediaMessagePdu) pdu).getBody();
353        } else {
354            throw new MmsException();
355        }
356    }
357
358    public void setCurrentMessageSize(int size) {
359        mCurrentMessageSize = size;
360    }
361
362    // getCurrentMessageSize returns the size of the message, not including resizable attachments
363    // such as photos. mCurrentMessageSize is used when adding/deleting/replacing non-resizable
364    // attachments (movies, sounds, etc) in order to compute how much size is left in the message.
365    // The difference between mCurrentMessageSize and the maxSize allowed for a message is then
366    // divided up between the remaining resizable attachments. While this function is public,
367    // it is only used internally between various MMS classes. If the UI wants to know the
368    // size of a MMS message, it should call getTotalMessageSize() instead.
369    public int getCurrentMessageSize() {
370        return mCurrentMessageSize;
371    }
372
373    // getTotalMessageSize returns the total size of the message, including resizable attachments
374    // such as photos. This function is intended to be used by the UI for displaying the size of the
375    // MMS message.
376    public int getTotalMessageSize() {
377        return mTotalMessageSize;
378    }
379
380    public void increaseMessageSize(int increaseSize) {
381        if (increaseSize > 0) {
382            mCurrentMessageSize += increaseSize;
383        }
384    }
385
386    public void decreaseMessageSize(int decreaseSize) {
387        if (decreaseSize > 0) {
388            mCurrentMessageSize -= decreaseSize;
389        }
390    }
391
392    public LayoutModel getLayout() {
393        return mLayout;
394    }
395
396    //
397    // Implement List<E> interface.
398    //
399    public boolean add(SlideModel object) {
400        int increaseSize = object.getSlideSize();
401        checkMessageSize(increaseSize);
402
403        if ((object != null) && mSlides.add(object)) {
404            increaseMessageSize(increaseSize);
405            object.registerModelChangedObserver(this);
406            for (IModelChangedObserver observer : mModelChangedObservers) {
407                object.registerModelChangedObserver(observer);
408            }
409            notifyModelChanged(true);
410            return true;
411        }
412        return false;
413    }
414
415    public boolean addAll(Collection<? extends SlideModel> collection) {
416        throw new UnsupportedOperationException("Operation not supported.");
417    }
418
419    public void clear() {
420        if (mSlides.size() > 0) {
421            for (SlideModel slide : mSlides) {
422                slide.unregisterModelChangedObserver(this);
423                for (IModelChangedObserver observer : mModelChangedObservers) {
424                    slide.unregisterModelChangedObserver(observer);
425                }
426            }
427            mCurrentMessageSize = 0;
428            mSlides.clear();
429            notifyModelChanged(true);
430        }
431    }
432
433    public boolean contains(Object object) {
434        return mSlides.contains(object);
435    }
436
437    public boolean containsAll(Collection<?> collection) {
438        return mSlides.containsAll(collection);
439    }
440
441    public boolean isEmpty() {
442        return mSlides.isEmpty();
443    }
444
445    public Iterator<SlideModel> iterator() {
446        return mSlides.iterator();
447    }
448
449    public boolean remove(Object object) {
450        if ((object != null) && mSlides.remove(object)) {
451            SlideModel slide = (SlideModel) object;
452            decreaseMessageSize(slide.getSlideSize());
453            slide.unregisterAllModelChangedObservers();
454            notifyModelChanged(true);
455            return true;
456        }
457        return false;
458    }
459
460    public boolean removeAll(Collection<?> collection) {
461        throw new UnsupportedOperationException("Operation not supported.");
462    }
463
464    public boolean retainAll(Collection<?> collection) {
465        throw new UnsupportedOperationException("Operation not supported.");
466    }
467
468    public int size() {
469        return mSlides.size();
470    }
471
472    public Object[] toArray() {
473        return mSlides.toArray();
474    }
475
476    public <T> T[] toArray(T[] array) {
477        return mSlides.toArray(array);
478    }
479
480    public void add(int location, SlideModel object) {
481        if (object != null) {
482            int increaseSize = object.getSlideSize();
483            checkMessageSize(increaseSize);
484
485            mSlides.add(location, object);
486            increaseMessageSize(increaseSize);
487            object.registerModelChangedObserver(this);
488            for (IModelChangedObserver observer : mModelChangedObservers) {
489                object.registerModelChangedObserver(observer);
490            }
491            notifyModelChanged(true);
492        }
493    }
494
495    public boolean addAll(int location,
496            Collection<? extends SlideModel> collection) {
497        throw new UnsupportedOperationException("Operation not supported.");
498    }
499
500    public SlideModel get(int location) {
501        return (location >= 0 && location < mSlides.size()) ? mSlides.get(location) : null;
502    }
503
504    public int indexOf(Object object) {
505        return mSlides.indexOf(object);
506    }
507
508    public int lastIndexOf(Object object) {
509        return mSlides.lastIndexOf(object);
510    }
511
512    public ListIterator<SlideModel> listIterator() {
513        return mSlides.listIterator();
514    }
515
516    public ListIterator<SlideModel> listIterator(int location) {
517        return mSlides.listIterator(location);
518    }
519
520    public SlideModel remove(int location) {
521        SlideModel slide = mSlides.remove(location);
522        if (slide != null) {
523            decreaseMessageSize(slide.getSlideSize());
524            slide.unregisterAllModelChangedObservers();
525            notifyModelChanged(true);
526        }
527        return slide;
528    }
529
530    public SlideModel set(int location, SlideModel object) {
531        SlideModel slide = mSlides.get(location);
532        if (null != object) {
533            int removeSize = 0;
534            int addSize = object.getSlideSize();
535            if (null != slide) {
536                removeSize = slide.getSlideSize();
537            }
538            if (addSize > removeSize) {
539                checkMessageSize(addSize - removeSize);
540                increaseMessageSize(addSize - removeSize);
541            } else {
542                decreaseMessageSize(removeSize - addSize);
543            }
544        }
545
546        slide =  mSlides.set(location, object);
547        if (slide != null) {
548            slide.unregisterAllModelChangedObservers();
549        }
550
551        if (object != null) {
552            object.registerModelChangedObserver(this);
553            for (IModelChangedObserver observer : mModelChangedObservers) {
554                object.registerModelChangedObserver(observer);
555            }
556        }
557
558        notifyModelChanged(true);
559        return slide;
560    }
561
562    public List<SlideModel> subList(int start, int end) {
563        return mSlides.subList(start, end);
564    }
565
566    @Override
567    protected void registerModelChangedObserverInDescendants(
568            IModelChangedObserver observer) {
569        mLayout.registerModelChangedObserver(observer);
570
571        for (SlideModel slide : mSlides) {
572            slide.registerModelChangedObserver(observer);
573        }
574    }
575
576    @Override
577    protected void unregisterModelChangedObserverInDescendants(
578            IModelChangedObserver observer) {
579        mLayout.unregisterModelChangedObserver(observer);
580
581        for (SlideModel slide : mSlides) {
582            slide.unregisterModelChangedObserver(observer);
583        }
584    }
585
586    @Override
587    protected void unregisterAllModelChangedObserversInDescendants() {
588        mLayout.unregisterAllModelChangedObservers();
589
590        for (SlideModel slide : mSlides) {
591            slide.unregisterAllModelChangedObservers();
592        }
593    }
594
595    public void onModelChanged(Model model, boolean dataChanged) {
596        if (dataChanged) {
597            mDocumentCache = null;
598            mPduBodyCache = null;
599        }
600    }
601
602    public void sync(PduBody pb) {
603        for (SlideModel slide : mSlides) {
604            for (MediaModel media : slide) {
605                PduPart part = pb.getPartByContentLocation(media.getSrc());
606                if (part != null) {
607                    media.setUri(part.getDataUri());
608                }
609            }
610        }
611    }
612
613    public void checkMessageSize(int increaseSize) throws ContentRestrictionException {
614        ContentRestriction cr = ContentRestrictionFactory.getContentRestriction();
615        cr.checkMessageSize(mCurrentMessageSize, increaseSize, mContext.getContentResolver());
616    }
617
618    /**
619     * Determines whether this is a "simple" slideshow.
620     * Criteria:
621     * - Exactly one slide
622     * - Exactly one multimedia attachment, but no audio
623     * - It can optionally have a caption
624    */
625    public boolean isSimple() {
626        // There must be one (and only one) slide.
627        if (size() != 1)
628            return false;
629
630        SlideModel slide = get(0);
631        // The slide must have either an image or video, but not both.
632        if (!(slide.hasImage() ^ slide.hasVideo()))
633            return false;
634
635        // No audio allowed.
636        if (slide.hasAudio())
637            return false;
638
639        return true;
640    }
641
642    /**
643     * Make sure the text in slide 0 is no longer holding onto a reference to the text
644     * in the message text box.
645     */
646    public void prepareForSend() {
647        if (size() == 1) {
648            TextModel text = get(0).getText();
649            if (text != null) {
650                text.cloneText();
651            }
652        }
653    }
654
655    /**
656     * Resize all the resizeable media objects to fit in the remaining size of the slideshow.
657     * This should be called off of the UI thread.
658     *
659     * @throws MmsException, ExceedMessageSizeException
660     */
661    public void finalResize(Uri messageUri) throws MmsException, ExceedMessageSizeException {
662
663        // Figure out if we have any media items that need to be resized and total up the
664        // sizes of the items that can't be resized.
665        int resizableCnt = 0;
666        int fixedSizeTotal = 0;
667        for (SlideModel slide : mSlides) {
668            for (MediaModel media : slide) {
669                if (media.getMediaResizable()) {
670                    ++resizableCnt;
671                } else {
672                    fixedSizeTotal += media.getMediaSize();
673                }
674            }
675        }
676        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
677            Log.v(TAG, "finalResize: original message size: " + getCurrentMessageSize() +
678                    " getMaxMessageSize: " + MmsConfig.getMaxMessageSize() +
679                    " fixedSizeTotal: " + fixedSizeTotal);
680        }
681        if (resizableCnt > 0) {
682            int remainingSize = MmsConfig.getMaxMessageSize() - fixedSizeTotal - SLIDESHOW_SLOP;
683            if (remainingSize <= 0) {
684                throw new ExceedMessageSizeException("No room for pictures");
685            }
686            long messageId = ContentUris.parseId(messageUri);
687            int bytesPerMediaItem = remainingSize / resizableCnt;
688            // Resize the resizable media items to fit within their byte limit.
689            for (SlideModel slide : mSlides) {
690                for (MediaModel media : slide) {
691                    if (media.getMediaResizable()) {
692                        media.resizeMedia(bytesPerMediaItem, messageId);
693                    }
694                }
695            }
696            // One last time through to calc the real message size.
697            int totalSize = 0;
698            for (SlideModel slide : mSlides) {
699                for (MediaModel media : slide) {
700                    totalSize += media.getMediaSize();
701                }
702            }
703            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
704                Log.v(TAG, "finalResize: new message size: " + totalSize);
705            }
706
707            if (totalSize > MmsConfig.getMaxMessageSize()) {
708                throw new ExceedMessageSizeException("After compressing pictures, message too big");
709            }
710            setCurrentMessageSize(totalSize);
711
712            onModelChanged(this, true);     // clear the cached pdu body
713            PduBody pb = toPduBody();
714            // This will write out all the new parts to:
715            //      /data/data/com.android.providers.telephony/app_parts
716            // and at the same time delete the old parts.
717            PduPersister.getPduPersister(mContext).updateParts(messageUri, pb);
718        }
719    }
720
721}
722