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