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 import static com.android.mms.dom.smil.SmilMediaElementImpl.SMIL_MEDIA_END_EVENT; 21 import static com.android.mms.dom.smil.SmilMediaElementImpl.SMIL_MEDIA_PAUSE_EVENT; 22 import static com.android.mms.dom.smil.SmilMediaElementImpl.SMIL_MEDIA_SEEK_EVENT; 23 import static com.android.mms.dom.smil.SmilMediaElementImpl.SMIL_MEDIA_START_EVENT; 24 import static com.android.mms.dom.smil.SmilParElementImpl.SMIL_SLIDE_END_EVENT; 25 import static com.android.mms.dom.smil.SmilParElementImpl.SMIL_SLIDE_START_EVENT; 26 27 import java.io.ByteArrayInputStream; 28 import java.io.ByteArrayOutputStream; 29 import java.io.IOException; 30 import java.util.ArrayList; 31 import java.util.Arrays; 32 33 import org.w3c.dom.events.EventTarget; 34 import org.w3c.dom.smil.SMILDocument; 35 import org.w3c.dom.smil.SMILElement; 36 import org.w3c.dom.smil.SMILLayoutElement; 37 import org.w3c.dom.smil.SMILMediaElement; 38 import org.w3c.dom.smil.SMILParElement; 39 import org.w3c.dom.smil.SMILRegionElement; 40 import org.w3c.dom.smil.SMILRegionMediaElement; 41 import org.w3c.dom.smil.SMILRootLayoutElement; 42 import org.xml.sax.SAXException; 43 44 import android.drm.DrmManagerClient; 45 import android.text.TextUtils; 46 import android.util.Config; 47 import android.util.Log; 48 49 import com.android.mms.MmsApp; 50 import com.android.mms.dom.smil.SmilDocumentImpl; 51 import com.android.mms.dom.smil.parser.SmilXmlParser; 52 import com.android.mms.dom.smil.parser.SmilXmlSerializer; 53 import com.google.android.mms.ContentType; 54 import com.google.android.mms.MmsException; 55 import com.google.android.mms.pdu.PduBody; 56 import com.google.android.mms.pdu.PduPart; 57 58 public class SmilHelper { 59 private static final String TAG = "Mms/smil"; 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 SmilHelper()69 private SmilHelper() { 70 // Never instantiate this class. 71 } 72 getDocument(PduBody pb)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 getDocument(SlideshowModel model)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 */ findSmilPart(PduBody body)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 validate(SMILDocument in)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 */ getSmilDocument(PduPart smilPart)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 addPar(SMILDocument document)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 createMediaElement( String tag, SMILDocument document, String src)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 escapeXML(String str)162 static public String escapeXML(String str) { 163 return str.replaceAll("&","&") 164 .replaceAll("<", "<") 165 .replaceAll(">", ">") 166 .replaceAll("\"", """) 167 .replaceAll("'", "'"); 168 } 169 createSmilDocument(PduBody pb)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 DrmManagerClient drmManagerClient = MmsApp.getApplication().getDrmManagerClient(); 202 203 boolean hasText = false; 204 boolean hasMedia = false; 205 for (int i = 0; i < partsNum; i++) { 206 // Create new <par> element. 207 if ((par == null) || (hasMedia && hasText)) { 208 par = addPar(document); 209 hasText = false; 210 hasMedia = false; 211 } 212 213 PduPart part = pb.getPart(i); 214 String contentType = new String(part.getContentType()); 215 216 if (ContentType.isDrmType(contentType)) { 217 contentType = drmManagerClient.getOriginalMimeType(part.getDataUri()); 218 } 219 220 if (contentType.equals(ContentType.TEXT_PLAIN) 221 || contentType.equalsIgnoreCase(ContentType.APP_WAP_XHTML) 222 || contentType.equals(ContentType.TEXT_HTML)) { 223 SMILMediaElement textElement = createMediaElement( 224 ELEMENT_TAG_TEXT, document, part.generateLocation()); 225 par.appendChild(textElement); 226 hasText = true; 227 } else if (ContentType.isImageType(contentType)) { 228 SMILMediaElement imageElement = createMediaElement( 229 ELEMENT_TAG_IMAGE, document, part.generateLocation()); 230 par.appendChild(imageElement); 231 hasMedia = true; 232 } else if (ContentType.isVideoType(contentType)) { 233 SMILMediaElement videoElement = createMediaElement( 234 ELEMENT_TAG_VIDEO, document, part.generateLocation()); 235 par.appendChild(videoElement); 236 hasMedia = true; 237 } else if (ContentType.isAudioType(contentType)) { 238 SMILMediaElement audioElement = createMediaElement( 239 ELEMENT_TAG_AUDIO, document, part.generateLocation()); 240 par.appendChild(audioElement); 241 hasMedia = true; 242 } else { 243 // TODO: handle other media types. 244 Log.w(TAG, "unsupport media type"); 245 } 246 } 247 248 return document; 249 } 250 createSmilDocument(SlideshowModel slideshow)251 private static SMILDocument createSmilDocument(SlideshowModel slideshow) { 252 if (Config.LOGV) { 253 Log.v(TAG, "Creating SMIL document from SlideshowModel."); 254 } 255 256 SMILDocument document = new SmilDocumentImpl(); 257 258 // Create SMIL and append it to document 259 SMILElement smilElement = (SMILElement) document.createElement("smil"); 260 document.appendChild(smilElement); 261 262 // Create HEAD and append it to SMIL 263 SMILElement headElement = (SMILElement) document.createElement("head"); 264 smilElement.appendChild(headElement); 265 266 // Create LAYOUT and append it to HEAD 267 SMILLayoutElement layoutElement = (SMILLayoutElement) 268 document.createElement("layout"); 269 headElement.appendChild(layoutElement); 270 271 // Create ROOT-LAYOUT and append it to LAYOUT 272 SMILRootLayoutElement rootLayoutElement = 273 (SMILRootLayoutElement) document.createElement("root-layout"); 274 LayoutModel layouts = slideshow.getLayout(); 275 rootLayoutElement.setWidth(layouts.getLayoutWidth()); 276 rootLayoutElement.setHeight(layouts.getLayoutHeight()); 277 String bgColor = layouts.getBackgroundColor(); 278 if (!TextUtils.isEmpty(bgColor)) { 279 rootLayoutElement.setBackgroundColor(bgColor); 280 } 281 layoutElement.appendChild(rootLayoutElement); 282 283 // Create REGIONs and append them to LAYOUT 284 ArrayList<RegionModel> regions = layouts.getRegions(); 285 ArrayList<SMILRegionElement> smilRegions = new ArrayList<SMILRegionElement>(); 286 for (RegionModel r : regions) { 287 SMILRegionElement smilRegion = (SMILRegionElement) document.createElement("region"); 288 smilRegion.setId(r.getRegionId()); 289 smilRegion.setLeft(r.getLeft()); 290 smilRegion.setTop(r.getTop()); 291 smilRegion.setWidth(r.getWidth()); 292 smilRegion.setHeight(r.getHeight()); 293 smilRegion.setFit(r.getFit()); 294 smilRegions.add(smilRegion); 295 } 296 297 // Create BODY and append it to the document. 298 SMILElement bodyElement = (SMILElement) document.createElement("body"); 299 smilElement.appendChild(bodyElement); 300 301 for (SlideModel slide : slideshow) { 302 boolean txtRegionPresentInLayout = false; 303 boolean imgRegionPresentInLayout = false; 304 305 // Create PAR element. 306 SMILParElement par = addPar(document); 307 par.setDur(slide.getDuration() / 1000f); 308 309 addParElementEventListeners((EventTarget) par, slide); 310 311 // Add all media elements. 312 for (MediaModel media : slide) { 313 SMILMediaElement sme = null; 314 String src = media.getSrc(); 315 if (media instanceof TextModel) { 316 TextModel text = (TextModel) media; 317 if (TextUtils.isEmpty(text.getText())) { 318 if (LOCAL_LOGV) { 319 Log.v(TAG, "Empty text part ignored: " + text.getSrc()); 320 } 321 continue; 322 } 323 sme = SmilHelper.createMediaElement(SmilHelper.ELEMENT_TAG_TEXT, document, src); 324 txtRegionPresentInLayout = setRegion((SMILRegionMediaElement) sme, 325 smilRegions, 326 layoutElement, 327 LayoutModel.TEXT_REGION_ID, 328 txtRegionPresentInLayout); 329 } else if (media instanceof ImageModel) { 330 sme = SmilHelper.createMediaElement(SmilHelper.ELEMENT_TAG_IMAGE, document, src); 331 imgRegionPresentInLayout = setRegion((SMILRegionMediaElement) sme, 332 smilRegions, 333 layoutElement, 334 LayoutModel.IMAGE_REGION_ID, 335 imgRegionPresentInLayout); 336 } else if (media instanceof VideoModel) { 337 sme = SmilHelper.createMediaElement(SmilHelper.ELEMENT_TAG_VIDEO, document, src); 338 imgRegionPresentInLayout = setRegion((SMILRegionMediaElement) sme, 339 smilRegions, 340 layoutElement, 341 LayoutModel.IMAGE_REGION_ID, 342 imgRegionPresentInLayout); 343 } else if (media instanceof AudioModel) { 344 sme = SmilHelper.createMediaElement(SmilHelper.ELEMENT_TAG_AUDIO, document, src); 345 } else { 346 Log.w(TAG, "Unsupport media: " + media); 347 continue; 348 } 349 350 // Set timing information. 351 int begin = media.getBegin(); 352 if (begin != 0) { 353 sme.setAttribute("begin", String.valueOf(begin / 1000)); 354 } 355 int duration = media.getDuration(); 356 if (duration != 0) { 357 sme.setDur((float) duration / 1000); 358 } 359 par.appendChild(sme); 360 361 addMediaElementEventListeners((EventTarget) sme, media); 362 } 363 } 364 365 if (LOCAL_LOGV) { 366 ByteArrayOutputStream out = new ByteArrayOutputStream(); 367 SmilXmlSerializer.serialize(document, out); 368 Log.v(TAG, out.toString()); 369 } 370 371 return document; 372 } 373 findRegionElementById( ArrayList<SMILRegionElement> smilRegions, String rId)374 private static SMILRegionElement findRegionElementById( 375 ArrayList<SMILRegionElement> smilRegions, String rId) { 376 for (SMILRegionElement smilRegion : smilRegions) { 377 if (smilRegion.getId().equals(rId)) { 378 return smilRegion; 379 } 380 } 381 return null; 382 } 383 setRegion(SMILRegionMediaElement srme, ArrayList<SMILRegionElement> smilRegions, SMILLayoutElement smilLayout, String regionId, boolean regionPresentInLayout)384 private static boolean setRegion(SMILRegionMediaElement srme, 385 ArrayList<SMILRegionElement> smilRegions, 386 SMILLayoutElement smilLayout, 387 String regionId, 388 boolean regionPresentInLayout) { 389 SMILRegionElement smilRegion = findRegionElementById(smilRegions, regionId); 390 if (!regionPresentInLayout && smilRegion != null) { 391 srme.setRegion(smilRegion); 392 smilLayout.appendChild(smilRegion); 393 return true; 394 } 395 return false; 396 } 397 addMediaElementEventListeners( EventTarget target, MediaModel media)398 static void addMediaElementEventListeners( 399 EventTarget target, MediaModel media) { 400 // To play the media with SmilPlayer, we should add them 401 // as EventListener into an EventTarget. 402 target.addEventListener(SMIL_MEDIA_START_EVENT, media, false); 403 target.addEventListener(SMIL_MEDIA_END_EVENT, media, false); 404 target.addEventListener(SMIL_MEDIA_PAUSE_EVENT, media, false); 405 target.addEventListener(SMIL_MEDIA_SEEK_EVENT, media, false); 406 } 407 addParElementEventListeners( EventTarget target, SlideModel slide)408 static void addParElementEventListeners( 409 EventTarget target, SlideModel slide) { 410 // To play the slide with SmilPlayer, we should add it 411 // as EventListener into an EventTarget. 412 target.addEventListener(SMIL_SLIDE_START_EVENT, slide, false); 413 target.addEventListener(SMIL_SLIDE_END_EVENT, slide, false); 414 } 415 } 416