SmilHelper.java revision 31e14b68beee7af348a4d1528acf947732351a51
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
20import static com.android.mms.dom.smil.SmilMediaElementImpl.SMIL_MEDIA_END_EVENT;
21import static com.android.mms.dom.smil.SmilMediaElementImpl.SMIL_MEDIA_PAUSE_EVENT;
22import static com.android.mms.dom.smil.SmilMediaElementImpl.SMIL_MEDIA_SEEK_EVENT;
23import static com.android.mms.dom.smil.SmilMediaElementImpl.SMIL_MEDIA_START_EVENT;
24import static com.android.mms.dom.smil.SmilParElementImpl.SMIL_SLIDE_END_EVENT;
25import static com.android.mms.dom.smil.SmilParElementImpl.SMIL_SLIDE_START_EVENT;
26
27import com.android.mms.dom.smil.SmilDocumentImpl;
28import com.android.mms.dom.smil.parser.SmilXmlParser;
29import com.android.mms.dom.smil.parser.SmilXmlSerializer;
30import com.android.mms.drm.DrmWrapper;
31import com.google.android.mms.ContentType;
32import com.google.android.mms.MmsException;
33import com.google.android.mms.pdu.PduBody;
34import com.google.android.mms.pdu.PduPart;
35
36import org.w3c.dom.events.EventTarget;
37import org.w3c.dom.smil.SMILDocument;
38import org.w3c.dom.smil.SMILElement;
39import org.w3c.dom.smil.SMILLayoutElement;
40import org.w3c.dom.smil.SMILMediaElement;
41import org.w3c.dom.smil.SMILParElement;
42import org.w3c.dom.smil.SMILRegionElement;
43import org.w3c.dom.smil.SMILRegionMediaElement;
44import org.w3c.dom.smil.SMILRootLayoutElement;
45import org.xml.sax.SAXException;
46
47import android.drm.mobile1.DrmException;
48import android.text.TextUtils;
49import android.util.Config;
50import android.util.Log;
51
52import java.io.ByteArrayInputStream;
53import java.io.ByteArrayOutputStream;
54import java.io.IOException;
55import java.util.ArrayList;
56import java.util.Arrays;
57
58public class SmilHelper {
59    private static final String TAG = "SmilHelper";
60    private static final boolean DEBUG = false;
61    private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV;
62
63    public static final String ELEMENT_TAG_TEXT = "text";
64    public static final String ELEMENT_TAG_IMAGE = "img";
65    public static final String ELEMENT_TAG_AUDIO = "audio";
66    public static final String ELEMENT_TAG_VIDEO = "video";
67    public static final String ELEMENT_TAG_REF = "ref";
68
69    private SmilHelper() {
70        // Never instantiate this class.
71    }
72
73    public static SMILDocument getDocument(PduBody pb) {
74        // Find SMIL part in the message.
75        PduPart smilPart = findSmilPart(pb);
76        SMILDocument document = null;
77
78        // Try to load SMIL document from existing part.
79        if (smilPart != null) {
80            document = getSmilDocument(smilPart);
81        }
82
83        if (document == null) {
84            // Create a new SMIL document.
85            document = createSmilDocument(pb);
86        }
87
88        return document;
89    }
90
91    public static SMILDocument getDocument(SlideshowModel model) {
92        return createSmilDocument(model);
93    }
94
95    /**
96     * Find a SMIL part in the MM.
97     *
98     * @return The existing SMIL part or null if no SMIL part was found.
99     */
100    private static PduPart findSmilPart(PduBody body) {
101        int partNum = body.getPartsNum();
102        for(int i = 0; i < partNum; i++) {
103            PduPart part = body.getPart(i);
104            if (Arrays.equals(part.getContentType(),
105                            ContentType.APP_SMIL.getBytes())) {
106                // Sure only one SMIL part.
107                return part;
108            }
109        }
110        return null;
111    }
112
113    private static SMILDocument validate(SMILDocument in) {
114        // TODO: add more validating facilities.
115        return in;
116    }
117
118    /**
119     * Parse SMIL message and retrieve SMILDocument.
120     *
121     * @return A SMILDocument or null if parsing failed.
122     */
123    private static SMILDocument getSmilDocument(PduPart smilPart) {
124        try {
125            byte[] data = smilPart.getData();
126            if (data != null) {
127                if (LOCAL_LOGV) {
128                    Log.v(TAG, "Parsing SMIL document.");
129                    Log.v(TAG, new String(data));
130                }
131
132                ByteArrayInputStream bais = new ByteArrayInputStream(data);
133                SMILDocument document = new SmilXmlParser().parse(bais);
134                return validate(document);
135            }
136        } catch (IOException e) {
137            Log.e(TAG, "Failed to parse SMIL document.", e);
138        } catch (SAXException e) {
139            Log.e(TAG, "Failed to parse SMIL document.", e);
140        } catch (MmsException e) {
141            Log.e(TAG, "Failed to parse SMIL document.", e);
142        }
143        return null;
144    }
145
146    public static SMILParElement addPar(SMILDocument document) {
147        SMILParElement par = (SMILParElement) document.createElement("par");
148        // Set duration to eight seconds by default.
149        par.setDur(8.0f);
150        document.getBody().appendChild(par);
151        return par;
152    }
153
154    public static SMILMediaElement createMediaElement(
155            String tag, SMILDocument document, String src) {
156        SMILMediaElement mediaElement =
157                (SMILMediaElement) document.createElement(tag);
158        mediaElement.setSrc(escapeXML(src));
159        return mediaElement;
160    }
161
162    static public String escapeXML(String str) {
163        return str.replaceAll("&","&amp;")
164                  .replaceAll("<", "&lt;")
165                  .replaceAll(">", "&gt;")
166                  .replaceAll("\"", "&quot;")
167                  .replaceAll("'", "&apos;");
168    }
169
170    private static SMILDocument createSmilDocument(PduBody pb) {
171        if (Config.LOGV) {
172            Log.v(TAG, "Creating default SMIL document.");
173        }
174
175        SMILDocument document = new SmilDocumentImpl();
176
177        // Create root element.
178        // FIXME: Should we create root element in the constructor of document?
179        SMILElement smil = (SMILElement) document.createElement("smil");
180        smil.setAttribute("xmlns", "http://www.w3.org/2001/SMIL20/Language");
181        document.appendChild(smil);
182
183        // Create <head> and <layout> element.
184        SMILElement head = (SMILElement) document.createElement("head");
185        smil.appendChild(head);
186
187        SMILLayoutElement layout = (SMILLayoutElement) document.createElement("layout");
188        head.appendChild(layout);
189
190        // Create <body> element and add a empty <par>.
191        SMILElement body = (SMILElement) document.createElement("body");
192        smil.appendChild(body);
193        SMILParElement par = addPar(document);
194
195        // Create media objects for the parts in PDU.
196        int partsNum = pb.getPartsNum();
197        if (partsNum == 0) {
198            return document;
199        }
200
201        boolean hasText = false;
202        boolean hasMedia = false;
203        for (int i = 0; i < partsNum; i++) {
204            // Create new <par> element.
205            if ((par == null) || (hasMedia && hasText)) {
206                par = addPar(document);
207                hasText = false;
208                hasMedia = false;
209            }
210
211            PduPart part = pb.getPart(i);
212            String contentType = new String(part.getContentType());
213            if (ContentType.isDrmType(contentType)) {
214                DrmWrapper dw;
215                try {
216                    dw = new DrmWrapper(contentType, part.getDataUri(),
217                                        part.getData());
218                    contentType = dw.getContentType();
219                } catch (DrmException e) {
220                    Log.e(TAG, e.getMessage(), e);
221                } catch (IOException e) {
222                    Log.e(TAG, e.getMessage(), e);
223                }
224            }
225
226            if (contentType.equals(ContentType.TEXT_PLAIN)
227                    || contentType.equalsIgnoreCase(ContentType.APP_WAP_XHTML)) {
228                SMILMediaElement textElement = createMediaElement(
229                        ELEMENT_TAG_TEXT, document, part.generateLocation());
230                par.appendChild(textElement);
231                hasText = true;
232            } else if (ContentType.isImageType(contentType)) {
233                SMILMediaElement imageElement = createMediaElement(
234                        ELEMENT_TAG_IMAGE, document, part.generateLocation());
235                par.appendChild(imageElement);
236                hasMedia = true;
237            } else if (ContentType.isVideoType(contentType)) {
238                SMILMediaElement videoElement = createMediaElement(
239                        ELEMENT_TAG_VIDEO, document, part.generateLocation());
240                par.appendChild(videoElement);
241                hasMedia = true;
242            } else if (ContentType.isAudioType(contentType)) {
243                SMILMediaElement audioElement = createMediaElement(
244                        ELEMENT_TAG_AUDIO, document, part.generateLocation());
245                par.appendChild(audioElement);
246                hasMedia = true;
247            } else {
248                // TODO: handle other media types.
249                Log.w(TAG, "unsupport media type");
250            }
251        }
252
253        return document;
254    }
255
256    private static SMILDocument createSmilDocument(SlideshowModel slideshow) {
257        if (Config.LOGV) {
258            Log.v(TAG, "Creating SMIL document from SlideshowModel.");
259        }
260
261        SMILDocument document = new SmilDocumentImpl();
262
263        // Create SMIL and append it to document
264        SMILElement smilElement = (SMILElement) document.createElement("smil");
265        document.appendChild(smilElement);
266
267        // Create HEAD and append it to SMIL
268        SMILElement headElement = (SMILElement) document.createElement("head");
269        smilElement.appendChild(headElement);
270
271        // Create LAYOUT and append it to HEAD
272        SMILLayoutElement layoutElement = (SMILLayoutElement)
273                document.createElement("layout");
274        headElement.appendChild(layoutElement);
275
276        // Create ROOT-LAYOUT and append it to LAYOUT
277        SMILRootLayoutElement rootLayoutElement =
278                (SMILRootLayoutElement) document.createElement("root-layout");
279        LayoutModel layouts = slideshow.getLayout();
280        rootLayoutElement.setWidth(layouts.getLayoutWidth());
281        rootLayoutElement.setHeight(layouts.getLayoutHeight());
282        String bgColor = layouts.getBackgroundColor();
283        if (!TextUtils.isEmpty(bgColor)) {
284            rootLayoutElement.setBackgroundColor(bgColor);
285        }
286        layoutElement.appendChild(rootLayoutElement);
287
288        // Create REGIONs and append them to LAYOUT
289        ArrayList<RegionModel> regions = layouts.getRegions();
290        ArrayList<SMILRegionElement> smilRegions = new ArrayList<SMILRegionElement>();
291        for (RegionModel r : regions) {
292            SMILRegionElement smilRegion = (SMILRegionElement) document.createElement("region");
293            smilRegion.setId(r.getRegionId());
294            smilRegion.setLeft(r.getLeft());
295            smilRegion.setTop(r.getTop());
296            smilRegion.setWidth(r.getWidth());
297            smilRegion.setHeight(r.getHeight());
298            smilRegion.setFit(r.getFit());
299            smilRegions.add(smilRegion);
300        }
301
302        // Create BODY and append it to the document.
303        SMILElement bodyElement = (SMILElement) document.createElement("body");
304        smilElement.appendChild(bodyElement);
305
306        boolean txtRegionPresentInLayout = false;
307        boolean imgRegionPresentInLayout = false;
308        for (SlideModel slide : slideshow) {
309            // Create PAR element.
310            SMILParElement par = addPar(document);
311            par.setDur(slide.getDuration() / 1000f);
312
313            addParElementEventListeners((EventTarget) par, slide);
314
315            // Add all media elements.
316            for (MediaModel media : slide) {
317                SMILMediaElement sme = null;
318                String src = media.getSrc();
319                if (media instanceof TextModel) {
320                    TextModel text = (TextModel) media;
321                    if (TextUtils.isEmpty(text.getText())) {
322                        if (LOCAL_LOGV) {
323                            Log.v(TAG, "Empty text part ignored: " + text.getSrc());
324                        }
325                        continue;
326                    }
327                    sme = SmilHelper.createMediaElement(SmilHelper.ELEMENT_TAG_TEXT, document, src);
328                    txtRegionPresentInLayout = setRegion((SMILRegionMediaElement) sme,
329                                                         smilRegions,
330                                                         layoutElement,
331                                                         LayoutModel.TEXT_REGION_ID,
332                                                         txtRegionPresentInLayout);
333                } else if (media instanceof ImageModel) {
334                    sme = SmilHelper.createMediaElement(SmilHelper.ELEMENT_TAG_IMAGE, document, src);
335                    imgRegionPresentInLayout = setRegion((SMILRegionMediaElement) sme,
336                                                         smilRegions,
337                                                         layoutElement,
338                                                         LayoutModel.IMAGE_REGION_ID,
339                                                         imgRegionPresentInLayout);
340                } else if (media instanceof VideoModel) {
341                    sme = SmilHelper.createMediaElement(SmilHelper.ELEMENT_TAG_VIDEO, document, src);
342                    imgRegionPresentInLayout = setRegion((SMILRegionMediaElement) sme,
343                                                         smilRegions,
344                                                         layoutElement,
345                                                         LayoutModel.IMAGE_REGION_ID,
346                                                         imgRegionPresentInLayout);
347                } else if (media instanceof AudioModel) {
348                    sme = SmilHelper.createMediaElement(SmilHelper.ELEMENT_TAG_AUDIO, document, src);
349                } else {
350                    Log.w(TAG, "Unsupport media: " + media);
351                    continue;
352                }
353
354                // Set timing information.
355                int begin = media.getBegin();
356                if (begin != 0) {
357                    sme.setAttribute("begin", String.valueOf(begin / 1000));
358                }
359                int duration = media.getDuration();
360                if (duration != 0) {
361                    sme.setDur((float) duration / 1000);
362                }
363                par.appendChild(sme);
364
365                addMediaElementEventListeners((EventTarget) sme, media);
366            }
367        }
368
369        if (LOCAL_LOGV) {
370            ByteArrayOutputStream out = new ByteArrayOutputStream();
371            SmilXmlSerializer.serialize(document, out);
372            Log.v(TAG, out.toString());
373        }
374
375        return document;
376    }
377
378    private static SMILRegionElement findRegionElementById(
379            ArrayList<SMILRegionElement> smilRegions, String rId) {
380        for (SMILRegionElement smilRegion : smilRegions) {
381            if (smilRegion.getId().equals(rId)) {
382                return smilRegion;
383            }
384        }
385        return null;
386    }
387
388    private static boolean setRegion(SMILRegionMediaElement srme,
389                                     ArrayList<SMILRegionElement> smilRegions,
390                                     SMILLayoutElement smilLayout,
391                                     String regionId,
392                                     boolean regionPresentInLayout) {
393        SMILRegionElement smilRegion = findRegionElementById(smilRegions, regionId);
394        if (!regionPresentInLayout && smilRegion != null) {
395            srme.setRegion(smilRegion);
396            smilLayout.appendChild(smilRegion);
397            return true;
398        }
399        return false;
400    }
401
402    static void addMediaElementEventListeners(
403            EventTarget target, MediaModel media) {
404        // To play the media with SmilPlayer, we should add them
405        // as EventListener into an EventTarget.
406        target.addEventListener(SMIL_MEDIA_START_EVENT, media, false);
407        target.addEventListener(SMIL_MEDIA_END_EVENT, media, false);
408        target.addEventListener(SMIL_MEDIA_PAUSE_EVENT, media, false);
409        target.addEventListener(SMIL_MEDIA_SEEK_EVENT, media, false);
410    }
411
412    static void addParElementEventListeners(
413            EventTarget target, SlideModel slide) {
414        // To play the slide with SmilPlayer, we should add it
415        // as EventListener into an EventTarget.
416        target.addEventListener(SMIL_SLIDE_START_EVENT, slide, false);
417        target.addEventListener(SMIL_SLIDE_END_EVENT, slide, false);
418    }
419}
420