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.ui; 19 20 import java.io.ByteArrayOutputStream; 21 22 import org.w3c.dom.NamedNodeMap; 23 import org.w3c.dom.Node; 24 import org.w3c.dom.NodeList; 25 import org.w3c.dom.events.Event; 26 import org.w3c.dom.events.EventListener; 27 import org.w3c.dom.events.EventTarget; 28 import org.w3c.dom.smil.SMILDocument; 29 import org.w3c.dom.smil.SMILElement; 30 31 import android.app.Activity; 32 import android.content.Intent; 33 import android.graphics.PixelFormat; 34 import android.net.Uri; 35 import android.os.Bundle; 36 import android.os.Handler; 37 import android.util.Log; 38 import android.view.KeyEvent; 39 import android.view.MotionEvent; 40 import android.view.View; 41 import android.view.View.OnClickListener; 42 import android.view.Window; 43 import android.widget.MediaController; 44 import android.widget.MediaController.MediaPlayerControl; 45 46 import com.android.mms.R; 47 import com.android.mms.dom.AttrImpl; 48 import com.android.mms.dom.smil.SmilDocumentImpl; 49 import com.android.mms.dom.smil.SmilPlayer; 50 import com.android.mms.dom.smil.parser.SmilXmlSerializer; 51 import com.android.mms.model.LayoutModel; 52 import com.android.mms.model.RegionModel; 53 import com.android.mms.model.SlideshowModel; 54 import com.android.mms.model.SmilHelper; 55 import com.google.android.mms.MmsException; 56 57 /** 58 * Plays the given slideshow in full-screen mode with a common controller. 59 */ 60 public class SlideshowActivity extends Activity implements EventListener { 61 private static final String TAG = "SlideshowActivity"; 62 private static final boolean DEBUG = false; 63 private static final boolean LOCAL_LOGV = false; 64 65 private MediaController mMediaController; 66 private SmilPlayer mSmilPlayer; 67 68 private Handler mHandler; 69 70 private SMILDocument mSmilDoc; 71 72 private SlideView mSlideView; 73 private int mSlideCount; 74 75 /** 76 * @return whether the Smil has MMS conformance layout. 77 * Refer to MMS Conformance Document OMA-MMS-CONF-v1_2-20050301-A 78 */ 79 private static final boolean isMMSConformance(SMILDocument smilDoc) { 80 SMILElement head = smilDoc.getHead(); 81 if (head == null) { 82 // No 'head' element 83 return false; 84 } 85 NodeList children = head.getChildNodes(); 86 if (children == null || children.getLength() != 1) { 87 // The 'head' element should have only one child. 88 return false; 89 } 90 Node layout = children.item(0); 91 if (layout == null || !"layout".equals(layout.getNodeName())) { 92 // The child is not layout element 93 return false; 94 } 95 NodeList layoutChildren = layout.getChildNodes(); 96 if (layoutChildren == null) { 97 // The 'layout' element has no child. 98 return false; 99 } 100 int num = layoutChildren.getLength(); 101 if (num <= 0) { 102 // The 'layout' element has no child. 103 return false; 104 } 105 for (int i = 0; i < num; i++) { 106 Node layoutChild = layoutChildren.item(i); 107 if (layoutChild == null) { 108 // The 'layout' child is null. 109 return false; 110 } 111 String name = layoutChild.getNodeName(); 112 if ("root-layout".equals(name)) { 113 continue; 114 } else if ("region".equals(name)) { 115 NamedNodeMap map = layoutChild.getAttributes(); 116 for (int j = 0; j < map.getLength(); j++) { 117 Node node = map.item(j); 118 if (node == null) { 119 return false; 120 } 121 String attrName = node.getNodeName(); 122 // The attr should be one of left, top, height, width, fit and id 123 if ("left".equals(attrName) || "top".equals(attrName) || 124 "height".equals(attrName) || "width".equals(attrName) || 125 "fit".equals(attrName)) { 126 continue; 127 } else if ("id".equals(attrName)) { 128 String value; 129 if (node instanceof AttrImpl) { 130 value = ((AttrImpl)node).getValue(); 131 } else { 132 return false; 133 } 134 if ("Text".equals(value) || "Image".equals(value)) { 135 continue; 136 } else { 137 // The id attr is not 'Text' or 'Image' 138 return false; 139 } 140 } else { 141 return false; 142 } 143 } 144 } else { 145 // The 'layout' element has the child other than 'root-layout' or 'region' 146 return false; 147 } 148 } 149 return true; 150 } 151 152 @Override 153 public void onCreate(Bundle icicle) { 154 super.onCreate(icicle); 155 mHandler = new Handler(); 156 157 // Play slide-show in full-screen mode. 158 requestWindowFeature(Window.FEATURE_NO_TITLE); 159 getWindow().setFormat(PixelFormat.TRANSLUCENT); 160 setContentView(R.layout.slideshow); 161 162 Intent intent = getIntent(); 163 Uri msg = intent.getData(); 164 final SlideshowModel model; 165 166 try { 167 model = SlideshowModel.createFromMessageUri(this, msg); 168 mSlideCount = model.size(); 169 } catch (MmsException e) { 170 Log.e(TAG, "Cannot present the slide show.", e); 171 finish(); 172 return; 173 } 174 175 mSlideView = (SlideView) findViewById(R.id.slide_view); 176 PresenterFactory.getPresenter("SlideshowPresenter", this, mSlideView, model); 177 178 mHandler.post(new Runnable() { 179 private boolean isRotating() { 180 return mSmilPlayer.isPausedState() 181 || mSmilPlayer.isPlayingState() 182 || mSmilPlayer.isPlayedState(); 183 } 184 185 public void run() { 186 mSmilPlayer = SmilPlayer.getPlayer(); 187 if (mSlideCount > 1) { 188 // Only show the slideshow controller if we have more than a single slide. 189 // Otherwise, when we play a sound on a single slide, it appears like 190 // the slide controller should control the sound (seeking, ff'ing, etc). 191 initMediaController(); 192 mSlideView.setMediaController(mMediaController); 193 } 194 // Use SmilHelper.getDocument() to ensure rebuilding the 195 // entire SMIL document. 196 mSmilDoc = SmilHelper.getDocument(model); 197 if (isMMSConformance(mSmilDoc)) { 198 int imageLeft = 0; 199 int imageTop = 0; 200 int textLeft = 0; 201 int textTop = 0; 202 LayoutModel layout = model.getLayout(); 203 if (layout != null) { 204 RegionModel imageRegion = layout.getImageRegion(); 205 if (imageRegion != null) { 206 imageLeft = imageRegion.getLeft(); 207 imageTop = imageRegion.getTop(); 208 } 209 RegionModel textRegion = layout.getTextRegion(); 210 if (textRegion != null) { 211 textLeft = textRegion.getLeft(); 212 textTop = textRegion.getTop(); 213 } 214 } 215 mSlideView.enableMMSConformanceMode(textLeft, textTop, imageLeft, imageTop); 216 } 217 if (DEBUG) { 218 ByteArrayOutputStream ostream = new ByteArrayOutputStream(); 219 SmilXmlSerializer.serialize(mSmilDoc, ostream); 220 if (LOCAL_LOGV) { 221 Log.v(TAG, ostream.toString()); 222 } 223 } 224 225 // Add event listener. 226 ((EventTarget) mSmilDoc).addEventListener( 227 SmilDocumentImpl.SMIL_DOCUMENT_END_EVENT, 228 SlideshowActivity.this, false); 229 230 mSmilPlayer.init(mSmilDoc); 231 if (isRotating()) { 232 mSmilPlayer.reload(); 233 } else { 234 mSmilPlayer.play(); 235 } 236 } 237 }); 238 } 239 240 private void initMediaController() { 241 mMediaController = new MediaController(SlideshowActivity.this, false); 242 mMediaController.setMediaPlayer(new SmilPlayerController(mSmilPlayer)); 243 mMediaController.setAnchorView(findViewById(R.id.slide_view)); 244 mMediaController.setPrevNextListeners( 245 new OnClickListener() { 246 public void onClick(View v) { 247 mSmilPlayer.next(); 248 } 249 }, 250 new OnClickListener() { 251 public void onClick(View v) { 252 mSmilPlayer.prev(); 253 } 254 }); 255 } 256 257 @Override 258 public boolean onTouchEvent(MotionEvent ev) { 259 if ((mSmilPlayer != null) && (mMediaController != null)) { 260 mMediaController.show(); 261 } 262 return false; 263 } 264 265 @Override 266 protected void onPause() { 267 super.onPause(); 268 if (mSmilDoc != null) { 269 ((EventTarget) mSmilDoc).removeEventListener( 270 SmilDocumentImpl.SMIL_DOCUMENT_END_EVENT, this, false); 271 } 272 if (mSmilPlayer != null) { 273 mSmilPlayer.pause(); 274 } 275 } 276 277 @Override 278 protected void onStop() { 279 super.onStop(); 280 if ((null != mSmilPlayer)) { 281 if (isFinishing()) { 282 mSmilPlayer.stop(); 283 } else { 284 mSmilPlayer.stopWhenReload(); 285 } 286 if (mMediaController != null) { 287 // Must do this so we don't leak a window. 288 mMediaController.hide(); 289 } 290 } 291 } 292 293 @Override 294 public boolean onKeyDown(int keyCode, KeyEvent event) { 295 switch (keyCode) { 296 case KeyEvent.KEYCODE_VOLUME_DOWN: 297 case KeyEvent.KEYCODE_VOLUME_UP: 298 case KeyEvent.KEYCODE_VOLUME_MUTE: 299 case KeyEvent.KEYCODE_DPAD_UP: 300 case KeyEvent.KEYCODE_DPAD_DOWN: 301 case KeyEvent.KEYCODE_DPAD_LEFT: 302 case KeyEvent.KEYCODE_DPAD_RIGHT: 303 break; 304 case KeyEvent.KEYCODE_BACK: 305 case KeyEvent.KEYCODE_MENU: 306 if ((mSmilPlayer != null) && 307 (mSmilPlayer.isPausedState() 308 || mSmilPlayer.isPlayingState() 309 || mSmilPlayer.isPlayedState())) { 310 mSmilPlayer.stop(); 311 } 312 break; 313 default: 314 if ((mSmilPlayer != null) && (mMediaController != null)) { 315 mMediaController.show(); 316 } 317 } 318 return super.onKeyDown(keyCode, event); 319 } 320 321 private class SmilPlayerController implements MediaPlayerControl { 322 private final SmilPlayer mPlayer; 323 /** 324 * We need to cache the playback state because when the MediaController issues a play or 325 * pause command, it expects subsequent calls to {@link #isPlaying()} to return the right 326 * value immediately. However, the SmilPlayer executes play and pause asynchronously, so 327 * {@link #isPlaying()} will return the wrong value for some time. That's why we keep our 328 * own version of the state of whether the player is playing. 329 * 330 * Initialized to true because we always programatically start the SmilPlayer upon creation 331 */ 332 private boolean mCachedIsPlaying = true; 333 334 public SmilPlayerController(SmilPlayer player) { 335 mPlayer = player; 336 } 337 338 public int getBufferPercentage() { 339 // We don't need to buffer data, always return 100%. 340 return 100; 341 } 342 343 public int getCurrentPosition() { 344 return mPlayer.getCurrentPosition(); 345 } 346 347 public int getDuration() { 348 return mPlayer.getDuration(); 349 } 350 351 public boolean isPlaying() { 352 return mCachedIsPlaying; 353 } 354 355 public void pause() { 356 mPlayer.pause(); 357 mCachedIsPlaying = false; 358 } 359 360 public void seekTo(int pos) { 361 // Don't need to support. 362 } 363 364 public void start() { 365 mPlayer.start(); 366 mCachedIsPlaying = true; 367 } 368 369 public boolean canPause() { 370 return true; 371 } 372 373 public boolean canSeekBackward() { 374 return true; 375 } 376 377 public boolean canSeekForward() { 378 return true; 379 } 380 381 @Override 382 public int getAudioSessionId() { 383 return 0; 384 } 385 } 386 387 public void handleEvent(Event evt) { 388 final Event event = evt; 389 mHandler.post(new Runnable() { 390 public void run() { 391 String type = event.getType(); 392 if(type.equals(SmilDocumentImpl.SMIL_DOCUMENT_END_EVENT)) { 393 finish(); 394 } 395 } 396 }); 397 } 398 } 399