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
18package com.android.mms.ui;
19
20import java.io.ByteArrayOutputStream;
21
22import org.w3c.dom.NamedNodeMap;
23import org.w3c.dom.Node;
24import org.w3c.dom.NodeList;
25import org.w3c.dom.events.Event;
26import org.w3c.dom.events.EventListener;
27import org.w3c.dom.events.EventTarget;
28import org.w3c.dom.smil.SMILDocument;
29import org.w3c.dom.smil.SMILElement;
30
31import android.app.Activity;
32import android.content.Intent;
33import android.graphics.PixelFormat;
34import android.net.Uri;
35import android.os.Bundle;
36import android.os.Handler;
37import android.util.Log;
38import android.view.KeyEvent;
39import android.view.MotionEvent;
40import android.view.View;
41import android.view.View.OnClickListener;
42import android.view.Window;
43import android.widget.MediaController;
44import android.widget.MediaController.MediaPlayerControl;
45import android.widget.SeekBar;
46
47import com.android.mms.R;
48import com.android.mms.dom.AttrImpl;
49import com.android.mms.dom.smil.SmilDocumentImpl;
50import com.android.mms.dom.smil.SmilPlayer;
51import com.android.mms.dom.smil.parser.SmilXmlSerializer;
52import com.android.mms.model.LayoutModel;
53import com.android.mms.model.RegionModel;
54import com.android.mms.model.SlideshowModel;
55import com.android.mms.model.SmilHelper;
56import com.google.android.mms.MmsException;
57
58/**
59 * Plays the given slideshow in full-screen mode with a common controller.
60 */
61public 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