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.LogTag;
48import com.android.mms.R;
49import com.android.mms.dom.AttrImpl;
50import com.android.mms.dom.smil.SmilDocumentImpl;
51import com.android.mms.dom.smil.SmilPlayer;
52import com.android.mms.dom.smil.parser.SmilXmlSerializer;
53import com.android.mms.model.LayoutModel;
54import com.android.mms.model.RegionModel;
55import com.android.mms.model.SlideshowModel;
56import com.android.mms.model.SmilHelper;
57import com.google.android.mms.MmsException;
58
59/**
60 * Plays the given slideshow in full-screen mode with a common controller.
61 */
62public 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