SlideshowModel.java revision f7e8281a223af6228e6399055a6197a1edd9bc3a
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                    SmilHelper.addMediaElementEventListeners(
161                            (EventTarget) sme, media);
162                    mediaSet.add(media);
163                } catch (DrmException e) {
164                    Log.e(TAG, e.getMessage(), e);
165                } catch (IOException e) {
166                    Log.e(TAG, e.getMessage(), e);
167                } catch (IllegalArgumentException e) {
168                    Log.e(TAG, e.getMessage(), e);
169                }
170            }
171
172            SlideModel slide = new SlideModel((int) (par.getDur() * 1000), mediaSet);
173            slide.setFill(par.getFill());
174            SmilHelper.addParElementEventListeners((EventTarget) par, slide);
175            slides.add(slide);
176        }
177
178        SlideshowModel slideshow = new SlideshowModel(layouts, slides, document, pb, context);
179        slideshow.registerModelChangedObserver(slideshow);
180        return slideshow;
181    }
182
183    public PduBody toPduBody() {
184        if (mPduBodyCache == null) {
185            mDocumentCache = SmilHelper.getDocument(this);
186            mPduBodyCache = makePduBody(mDocumentCache);
187        }
188        return mPduBodyCache;
189    }
190
191    private PduBody makePduBody(SMILDocument document) {
192        return makePduBody(null, document, false);
193    }
194
195    private PduBody makePduBody(Context context, SMILDocument document, boolean isMakingCopy) {
196        PduBody pb = new PduBody();
197
198        boolean hasForwardLock = false;
199        for (SlideModel slide : mSlides) {
200            for (MediaModel media : slide) {
201                if (isMakingCopy) {
202                    if (media.isDrmProtected() && !media.isAllowedToForward()) {
203                        hasForwardLock = true;
204                        continue;
205                    }
206                }
207
208                PduPart part = new PduPart();
209
210                if (media.isText()) {
211                    TextModel text = (TextModel) media;
212                    // Don't create empty text part.
213                    if (TextUtils.isEmpty(text.getText())) {
214                        continue;
215                    }
216                    // Set Charset if it's a text media.
217                    part.setCharset(text.getCharset());
218                }
219
220                // Set Content-Type.
221                part.setContentType(media.getContentType().getBytes());
222
223                String src = media.getSrc();
224                String location;
225                boolean startWithContentId = src.startsWith("cid:");
226                if (startWithContentId) {
227                    location = src.substring("cid:".length());
228                } else {
229                    location = src;
230                }
231
232                // Set Content-Location.
233                part.setContentLocation(location.getBytes());
234
235                // Set Content-Id.
236                if (startWithContentId) {
237                    //Keep the original Content-Id.
238                    part.setContentId(location.getBytes());
239                }
240                else {
241                    int index = location.lastIndexOf(".");
242                    String contentId = (index == -1) ? location
243                            : location.substring(0, index);
244                    part.setContentId(contentId.getBytes());
245                }
246
247                if (media.isDrmProtected()) {
248                    DrmWrapper wrapper = media.getDrmObject();
249                    part.setDataUri(wrapper.getOriginalUri());
250                    part.setData(wrapper.getOriginalData());
251                } else if (media.isText()) {
252                    part.setData(((TextModel) media).getText().getBytes());
253                } else if (media.isImage() || media.isVideo() || media.isAudio()) {
254                    part.setDataUri(media.getUri());
255                } else {
256                    Log.w(TAG, "Unsupport media: " + media);
257                }
258
259                pb.addPart(part);
260            }
261        }
262
263        if (hasForwardLock && isMakingCopy && context != null) {
264            Toast.makeText(context,
265                    context.getString(R.string.cannot_forward_drm_obj),
266                    Toast.LENGTH_LONG).show();
267            document = SmilHelper.getDocument(pb);
268        }
269
270        // Create and insert SMIL part(as the first part) into the PduBody.
271        ByteArrayOutputStream out = new ByteArrayOutputStream();
272        SmilXmlSerializer.serialize(document, out);
273        PduPart smilPart = new PduPart();
274        smilPart.setContentId("smil".getBytes());
275        smilPart.setContentLocation("smil.xml".getBytes());
276        smilPart.setContentType(ContentType.APP_SMIL.getBytes());
277        smilPart.setData(out.toByteArray());
278        pb.addPart(0, smilPart);
279
280        return pb;
281    }
282
283    public PduBody makeCopy(Context context) {
284        return makePduBody(context, SmilHelper.getDocument(this), true);
285    }
286
287    public SMILDocument toSmilDocument() {
288        if (mDocumentCache == null) {
289            mDocumentCache = SmilHelper.getDocument(this);
290        }
291        return mDocumentCache;
292    }
293
294    public static PduBody getPduBody(Context context, Uri msg) throws MmsException {
295        PduPersister p = PduPersister.getPduPersister(context);
296        GenericPdu pdu = p.load(msg);
297
298        int msgType = pdu.getMessageType();
299        if ((msgType == PduHeaders.MESSAGE_TYPE_SEND_REQ)
300                || (msgType == PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF)) {
301            return ((MultimediaMessagePdu) pdu).getBody();
302        } else {
303            throw new MmsException();
304        }
305    }
306
307    public void setCurrentMessageSize(int size) {
308        mCurrentMessageSize = size;
309    }
310
311    public int getCurrentMessageSize() {
312        return mCurrentMessageSize;
313    }
314
315    public void increaseMessageSize(int increaseSize) {
316        if (increaseSize > 0) {
317            mCurrentMessageSize += increaseSize;
318        }
319    }
320
321    public void decreaseMessageSize(int decreaseSize) {
322        if (decreaseSize > 0) {
323            mCurrentMessageSize -= decreaseSize;
324        }
325    }
326
327    public LayoutModel getLayout() {
328        return mLayout;
329    }
330
331    //
332    // Implement List<E> interface.
333    //
334    public boolean add(SlideModel object) {
335        int increaseSize = object.getSlideSize();
336        checkMessageSize(increaseSize);
337
338        if ((object != null) && mSlides.add(object)) {
339            increaseMessageSize(increaseSize);
340            object.registerModelChangedObserver(this);
341            for (IModelChangedObserver observer : mModelChangedObservers) {
342                object.registerModelChangedObserver(observer);
343            }
344            notifyModelChanged(true);
345            return true;
346        }
347        return false;
348    }
349
350    public boolean addAll(Collection<? extends SlideModel> collection) {
351        throw new UnsupportedOperationException("Operation not supported.");
352    }
353
354    public void clear() {
355        if (mSlides.size() > 0) {
356            for (SlideModel slide : mSlides) {
357                slide.unregisterModelChangedObserver(this);
358                for (IModelChangedObserver observer : mModelChangedObservers) {
359                    slide.unregisterModelChangedObserver(observer);
360                }
361            }
362            mCurrentMessageSize = 0;
363            mSlides.clear();
364            notifyModelChanged(true);
365        }
366    }
367
368    public boolean contains(Object object) {
369        return mSlides.contains(object);
370    }
371
372    public boolean containsAll(Collection<?> collection) {
373        return mSlides.containsAll(collection);
374    }
375
376    public boolean isEmpty() {
377        return mSlides.isEmpty();
378    }
379
380    public Iterator<SlideModel> iterator() {
381        return mSlides.iterator();
382    }
383
384    public boolean remove(Object object) {
385        if ((object != null) && mSlides.remove(object)) {
386            SlideModel slide = (SlideModel) object;
387            decreaseMessageSize(slide.getSlideSize());
388            slide.unregisterAllModelChangedObservers();
389            notifyModelChanged(true);
390            return true;
391        }
392        return false;
393    }
394
395    public boolean removeAll(Collection<?> collection) {
396        throw new UnsupportedOperationException("Operation not supported.");
397    }
398
399    public boolean retainAll(Collection<?> collection) {
400        throw new UnsupportedOperationException("Operation not supported.");
401    }
402
403    public int size() {
404        return mSlides.size();
405    }
406
407    public Object[] toArray() {
408        return mSlides.toArray();
409    }
410
411    public <T> T[] toArray(T[] array) {
412        return mSlides.toArray(array);
413    }
414
415    public void add(int location, SlideModel object) {
416        if (object != null) {
417            int increaseSize = object.getSlideSize();
418            checkMessageSize(increaseSize);
419
420            mSlides.add(location, object);
421            increaseMessageSize(increaseSize);
422            object.registerModelChangedObserver(this);
423            for (IModelChangedObserver observer : mModelChangedObservers) {
424                object.registerModelChangedObserver(observer);
425            }
426            notifyModelChanged(true);
427        }
428    }
429
430    public boolean addAll(int location,
431            Collection<? extends SlideModel> collection) {
432        throw new UnsupportedOperationException("Operation not supported.");
433    }
434
435    public SlideModel get(int location) {
436        return (location >= 0 && location < mSlides.size()) ? mSlides.get(location) : null;
437    }
438
439    public int indexOf(Object object) {
440        return mSlides.indexOf(object);
441    }
442
443    public int lastIndexOf(Object object) {
444        return mSlides.lastIndexOf(object);
445    }
446
447    public ListIterator<SlideModel> listIterator() {
448        return mSlides.listIterator();
449    }
450
451    public ListIterator<SlideModel> listIterator(int location) {
452        return mSlides.listIterator(location);
453    }
454
455    public SlideModel remove(int location) {
456        SlideModel slide = mSlides.remove(location);
457        if (slide != null) {
458            decreaseMessageSize(slide.getSlideSize());
459            slide.unregisterAllModelChangedObservers();
460            notifyModelChanged(true);
461        }
462        return slide;
463    }
464
465    public SlideModel set(int location, SlideModel object) {
466        SlideModel slide = mSlides.get(location);
467        if (null != object) {
468            int removeSize = 0;
469            int addSize = object.getSlideSize();
470            if (null != slide) {
471                removeSize = slide.getSlideSize();
472            }
473            if (addSize > removeSize) {
474                checkMessageSize(addSize - removeSize);
475                increaseMessageSize(addSize - removeSize);
476            } else {
477                decreaseMessageSize(removeSize - addSize);
478            }
479        }
480
481        slide =  mSlides.set(location, object);
482        if (slide != null) {
483            slide.unregisterAllModelChangedObservers();
484        }
485
486        if (object != null) {
487            object.registerModelChangedObserver(this);
488            for (IModelChangedObserver observer : mModelChangedObservers) {
489                object.registerModelChangedObserver(observer);
490            }
491        }
492
493        notifyModelChanged(true);
494        return slide;
495    }
496
497    public List<SlideModel> subList(int start, int end) {
498        return mSlides.subList(start, end);
499    }
500
501    @Override
502    protected void registerModelChangedObserverInDescendants(
503            IModelChangedObserver observer) {
504        mLayout.registerModelChangedObserver(observer);
505
506        for (SlideModel slide : mSlides) {
507            slide.registerModelChangedObserver(observer);
508        }
509    }
510
511    @Override
512    protected void unregisterModelChangedObserverInDescendants(
513            IModelChangedObserver observer) {
514        mLayout.unregisterModelChangedObserver(observer);
515
516        for (SlideModel slide : mSlides) {
517            slide.unregisterModelChangedObserver(observer);
518        }
519    }
520
521    @Override
522    protected void unregisterAllModelChangedObserversInDescendants() {
523        mLayout.unregisterAllModelChangedObservers();
524
525        for (SlideModel slide : mSlides) {
526            slide.unregisterAllModelChangedObservers();
527        }
528    }
529
530    public void onModelChanged(Model model, boolean dataChanged) {
531        if (dataChanged) {
532            mDocumentCache = null;
533            mPduBodyCache = null;
534        }
535    }
536
537    public void sync(PduBody pb) {
538        for (SlideModel slide : mSlides) {
539            for (MediaModel media : slide) {
540                PduPart part = pb.getPartByContentLocation(media.getSrc());
541                if (part != null) {
542                    media.setUri(part.getDataUri());
543                }
544            }
545        }
546    }
547
548    public void checkMessageSize(int increaseSize) throws ContentRestrictionException {
549        ContentRestriction cr = ContentRestrictionFactory.getContentRestriction();
550        cr.checkMessageSize(mCurrentMessageSize, increaseSize, mContext.getContentResolver());
551    }
552
553    /**
554     * Determines whether this is a "simple" slideshow.
555     * Criteria:
556     * - Exactly one slide
557     * - Exactly one multimedia attachment, but no audio
558     * - It can optionally have a caption
559    */
560    public boolean isSimple() {
561        // There must be one (and only one) slide.
562        if (size() != 1)
563            return false;
564
565        SlideModel slide = get(0);
566        // The slide must have either an image or video, but not both.
567        if (!(slide.hasImage() ^ slide.hasVideo()))
568            return false;
569
570        // No audio allowed.
571        if (slide.hasAudio())
572            return false;
573
574        return true;
575    }
576
577    /**
578     * Make sure the text in slide 0 is no longer holding onto a reference to the text
579     * in the message text box.
580     */
581    public void prepareForSend() {
582        if (size() == 1) {
583            TextModel text = get(0).getText();
584            if (text != null) {
585                text.cloneText();
586            }
587        }
588    }
589
590    /**
591     * Resize all the resizeable media objects to fit in the remaining size of the slideshow.
592     * This should be called off of the UI thread.
593     *
594     * @throws MmsException, ExceedMessageSizeException
595     */
596    public void finalResize(Uri messageUri) throws MmsException, ExceedMessageSizeException {
597//        Log.v(TAG, "Original message size: " + getCurrentMessageSize() + " getMaxMessageSize: "
598//                + MmsConfig.getMaxMessageSize());
599
600        // Figure out if we have any media items that need to be resized and total up the
601        // sizes of the items that can't be resized.
602        int resizableCnt = 0;
603        int fixedSizeTotal = 0;
604        for (SlideModel slide : mSlides) {
605            for (MediaModel media : slide) {
606                if (media.getMediaResizable()) {
607                    ++resizableCnt;
608                } else {
609                    fixedSizeTotal += media.getMediaSize();
610                }
611            }
612        }
613        if (resizableCnt > 0) {
614            int remainingSize = MmsConfig.getMaxMessageSize() - fixedSizeTotal - SLIDESHOW_SLOP;
615            if (remainingSize <= 0) {
616                throw new ExceedMessageSizeException("No room for pictures");
617            }
618            long messageId = ContentUris.parseId(messageUri);
619            int bytesPerMediaItem = remainingSize / resizableCnt;
620            // Resize the resizable media items to fit within their byte limit.
621            for (SlideModel slide : mSlides) {
622                for (MediaModel media : slide) {
623                    if (media.getMediaResizable()) {
624                        media.resizeMedia(bytesPerMediaItem, messageId);
625                    }
626                }
627            }
628            // One last time through to calc the real message size.
629            int totalSize = 0;
630            for (SlideModel slide : mSlides) {
631                for (MediaModel media : slide) {
632                    totalSize += media.getMediaSize();
633                }
634            }
635//            Log.v(TAG, "New message size: " + totalSize + " getMaxMessageSize: "
636//                    + MmsConfig.getMaxMessageSize());
637
638            if (totalSize > MmsConfig.getMaxMessageSize()) {
639                throw new ExceedMessageSizeException("After compressing pictures, message too big");
640            }
641            setCurrentMessageSize(totalSize);
642
643            onModelChanged(this, true);     // clear the cached pdu body
644            PduBody pb = toPduBody();
645            // This will write out all the new parts to:
646            //      /data/data/com.android.providers.telephony/app_parts
647            // and at the same time delete the old parts.
648            PduPersister.getPduPersister(mContext).updateParts(messageUri, pb);
649        }
650    }
651
652}
653