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.LogTag; 50 import com.android.mms.MmsApp; 51 import com.android.mms.dom.smil.SmilDocumentImpl; 52 import com.android.mms.dom.smil.parser.SmilXmlParser; 53 import com.android.mms.dom.smil.parser.SmilXmlSerializer; 54 import com.google.android.mms.ContentType; 55 import com.google.android.mms.MmsException; 56 import com.google.android.mms.pdu.PduBody; 57 import com.google.android.mms.pdu.PduPart; 58 59 public 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 SmilHelper()70 private SmilHelper() { 71 // Never instantiate this class. 72 } 73 getDocument(PduBody pb)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 getDocument(SlideshowModel model)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 */ findSmilPart(PduBody body)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 validate(SMILDocument in)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 */ getSmilDocument(PduPart smilPart)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 addPar(SMILDocument document)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 createMediaElement( String tag, SMILDocument document, String src)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 escapeXML(String str)163 static public String escapeXML(String str) { 164 return str.replaceAll("&","&") 165 .replaceAll("<", "<") 166 .replaceAll(">", ">") 167 .replaceAll("\"", """) 168 .replaceAll("'", "'"); 169 } 170 createSmilDocument(PduBody pb)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 createSmilDocument(SlideshowModel slideshow)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 findRegionElementById( ArrayList<SMILRegionElement> smilRegions, String rId)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 setRegion(SMILRegionMediaElement srme, ArrayList<SMILRegionElement> smilRegions, SMILLayoutElement smilLayout, String regionId, boolean regionPresentInLayout)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 addMediaElementEventListeners( EventTarget target, MediaModel media)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 addParElementEventListeners( EventTarget target, SlideModel slide)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