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