Home | History | Annotate | Download | only in model
      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 
     18 package com.android.mms.model;
     19 
     20 
     21 import com.android.mms.ContentRestrictionException;
     22 import com.android.mms.ExceedMessageSizeException;
     23 import com.android.mms.MmsConfig;
     24 import com.android.mms.R;
     25 import com.android.mms.dom.smil.parser.SmilXmlSerializer;
     26 import android.drm.mobile1.DrmException;
     27 import com.android.mms.drm.DrmWrapper;
     28 import com.android.mms.layout.LayoutManager;
     29 import com.google.android.mms.ContentType;
     30 import com.google.android.mms.MmsException;
     31 import com.google.android.mms.pdu.GenericPdu;
     32 import com.google.android.mms.pdu.MultimediaMessagePdu;
     33 import com.google.android.mms.pdu.PduBody;
     34 import com.google.android.mms.pdu.PduHeaders;
     35 import com.google.android.mms.pdu.PduPart;
     36 import com.google.android.mms.pdu.PduPersister;
     37 
     38 import org.w3c.dom.NodeList;
     39 import org.w3c.dom.events.EventTarget;
     40 import org.w3c.dom.smil.SMILDocument;
     41 import org.w3c.dom.smil.SMILElement;
     42 import org.w3c.dom.smil.SMILLayoutElement;
     43 import org.w3c.dom.smil.SMILMediaElement;
     44 import org.w3c.dom.smil.SMILParElement;
     45 import org.w3c.dom.smil.SMILRegionElement;
     46 import org.w3c.dom.smil.SMILRootLayoutElement;
     47 
     48 import android.content.ContentUris;
     49 import android.content.Context;
     50 import android.net.Uri;
     51 import android.text.TextUtils;
     52 import android.util.Log;
     53 import android.widget.Toast;
     54 
     55 import java.io.ByteArrayOutputStream;
     56 import java.io.IOException;
     57 import java.util.ArrayList;
     58 import java.util.Collection;
     59 import java.util.Iterator;
     60 import java.util.List;
     61 import java.util.ListIterator;
     62 
     63 public 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