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