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