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