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 com.android.mms.R;
21import com.android.mms.layout.LayoutManager;
22
23import android.content.Context;
24import android.graphics.Bitmap;
25import android.graphics.BitmapFactory;
26import android.media.MediaPlayer;
27import android.net.Uri;
28import android.text.method.HideReturnsTransformationMethod;
29import android.util.AttributeSet;
30import android.util.Config;
31import android.util.Log;
32import android.view.Gravity;
33import android.view.LayoutInflater;
34import android.view.View;
35import android.widget.AbsoluteLayout;
36import android.widget.FrameLayout;
37import android.widget.ImageView;
38import android.widget.LinearLayout;
39import android.widget.MediaController;
40import android.widget.ScrollView;
41import android.widget.TextView;
42import android.widget.VideoView;
43
44import java.io.IOException;
45import java.util.Comparator;
46import java.util.Map;
47import java.util.TreeMap;
48
49/**
50 * A basic view to show the contents of a slide.
51 */
52public class SlideView extends AbsoluteLayout implements
53        AdaptableSlideViewInterface {
54    private static final String TAG = "SlideView";
55    private static final boolean DEBUG = false;
56    private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV;
57    // FIXME: Need getHeight from mAudioInfoView instead of constant AUDIO_INFO_HEIGHT.
58    private static final int AUDIO_INFO_HEIGHT = 82;
59
60    private View mAudioInfoView;
61    private ImageView mImageView;
62    private VideoView mVideoView;
63    private ScrollView mScrollText;
64    private TextView mTextView;
65    private OnSizeChangedListener mSizeChangedListener;
66    private MediaPlayer mAudioPlayer;
67    private boolean mIsPrepared;
68    private boolean mStartWhenPrepared;
69    private int     mSeekWhenPrepared;
70    private boolean mStopWhenPrepared;
71    private ScrollView mScrollViewPort;
72    private LinearLayout mViewPort;
73    // Indicates whether the view is in MMS conformance mode.
74    private boolean mConformanceMode;
75    private MediaController mMediaController;
76
77    MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() {
78        public void onPrepared(MediaPlayer mp) {
79            mIsPrepared = true;
80            if (mSeekWhenPrepared > 0) {
81                mAudioPlayer.seekTo(mSeekWhenPrepared);
82                mSeekWhenPrepared = 0;
83            }
84            if (mStartWhenPrepared) {
85                mAudioPlayer.start();
86                mStartWhenPrepared = false;
87                displayAudioInfo();
88            }
89            if (mStopWhenPrepared) {
90                mAudioPlayer.stop();
91                mAudioPlayer.release();
92                mAudioPlayer = null;
93                mStopWhenPrepared = false;
94                hideAudioInfo();
95            }
96        }
97    };
98
99    public SlideView(Context context) {
100        super(context);
101    }
102
103    public SlideView(Context context, AttributeSet attrs) {
104        super(context, attrs);
105    }
106
107    public void setImage(String name, Bitmap bitmap) {
108        if (mImageView == null) {
109            mImageView = new ImageView(mContext);
110            mImageView.setPadding(0, 5, 0, 5);
111            addView(mImageView, new LayoutParams(
112                    LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0));
113            if (DEBUG) {
114                mImageView.setBackgroundColor(0xFFFF0000);
115            }
116        }
117        try {
118            if (null == bitmap) {
119                bitmap = BitmapFactory.decodeResource(getResources(),
120                        R.drawable.ic_missing_thumbnail_picture);
121            }
122            mImageView.setVisibility(View.VISIBLE);
123            mImageView.setImageBitmap(bitmap);
124        } catch (java.lang.OutOfMemoryError e) {
125            Log.e(TAG, "setImage: out of memory: ", e);
126        }
127    }
128
129    public void setImageRegion(int left, int top, int width, int height) {
130        // Ignore any requirement of layout change once we are in MMS conformance mode.
131        if (mImageView != null && !mConformanceMode) {
132            mImageView.setLayoutParams(new LayoutParams(width, height, left, top));
133        }
134    }
135
136    public void setImageRegionFit(String fit) {
137        // TODO Auto-generated method stub
138    }
139
140    public void setVideo(String name, Uri video) {
141        if (mVideoView == null) {
142            mVideoView = new VideoView(mContext);
143            addView(mVideoView, new LayoutParams(
144                    LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0));
145            if (DEBUG) {
146                mVideoView.setBackgroundColor(0xFFFF0000);
147            }
148        }
149
150        if (LOCAL_LOGV) {
151            Log.v(TAG, "Changing video source to " + video);
152        }
153        mVideoView.setVisibility(View.VISIBLE);
154        mVideoView.setVideoURI(video);
155    }
156
157    public void setMediaController(MediaController mediaController) {
158        mMediaController = mediaController;
159    }
160
161    private void initAudioInfoView(String name) {
162        if (null == mAudioInfoView) {
163            LayoutInflater factory = LayoutInflater.from(getContext());
164            mAudioInfoView = factory.inflate(R.layout.playing_audio_info, null);
165            int height = mAudioInfoView.getHeight();
166            TextView audioName = (TextView) mAudioInfoView.findViewById(R.id.name);
167            audioName.setText(name);
168            if (mConformanceMode) {
169                mViewPort.addView(mAudioInfoView, new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT,
170                        AUDIO_INFO_HEIGHT));
171            } else {
172                addView(mAudioInfoView, new LayoutParams(
173                        LayoutParams.MATCH_PARENT, AUDIO_INFO_HEIGHT,
174                        0, getHeight() - AUDIO_INFO_HEIGHT));
175                if (DEBUG) {
176                    mAudioInfoView.setBackgroundColor(0xFFFF0000);
177                }
178            }
179        }
180        mAudioInfoView.setVisibility(View.GONE);
181    }
182
183    private void displayAudioInfo() {
184        if (null != mAudioInfoView) {
185            mAudioInfoView.setVisibility(View.VISIBLE);
186        }
187    }
188
189    private void hideAudioInfo() {
190        if (null != mAudioInfoView) {
191            mAudioInfoView.setVisibility(View.GONE);
192        }
193    }
194
195    public void setAudio(Uri audio, String name, Map<String, ?> extras) {
196        if (audio == null) {
197            throw new IllegalArgumentException("Audio URI may not be null.");
198        }
199
200        if (LOCAL_LOGV) {
201            Log.v(TAG, "Changing audio source to " + audio);
202        }
203
204        if (mAudioPlayer != null) {
205            mAudioPlayer.reset();
206            mAudioPlayer.release();
207            mAudioPlayer = null;
208        }
209        mIsPrepared = false;
210
211        try {
212            mAudioPlayer = new MediaPlayer();
213            mAudioPlayer.setOnPreparedListener(mPreparedListener);
214            mAudioPlayer.setDataSource(mContext, audio);
215            mAudioPlayer.prepareAsync();
216        } catch (IOException e) {
217            Log.e(TAG, "Unexpected IOException.", e);
218            mAudioPlayer.release();
219            mAudioPlayer = null;
220        }
221        initAudioInfoView(name);
222    }
223
224    public void setText(String name, String text) {
225        if (!mConformanceMode) {
226            if (null == mScrollText) {
227                mScrollText = new ScrollView(mContext);
228                mScrollText.setScrollBarStyle(SCROLLBARS_OUTSIDE_INSET);
229                addView(mScrollText, new LayoutParams(
230                        LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0));
231                if (DEBUG) {
232                    mScrollText.setBackgroundColor(0xFF00FF00);
233                }
234            }
235            if (null == mTextView) {
236                mTextView = new TextView(mContext);
237                mTextView.setTransformationMethod(HideReturnsTransformationMethod.getInstance());
238                mScrollText.addView(mTextView);
239            }
240            mScrollText.requestFocus();
241        }
242        mTextView.setVisibility(View.VISIBLE);
243        mTextView.setText(text);
244    }
245
246    public void setTextRegion(int left, int top, int width, int height) {
247        // Ignore any requirement of layout change once we are in MMS conformance mode.
248        if (mScrollText != null && !mConformanceMode) {
249            mScrollText.setLayoutParams(new LayoutParams(width, height, left, top));
250        }
251    }
252
253    public void setVideoRegion(int left, int top, int width, int height) {
254        if (mVideoView != null && !mConformanceMode) {
255            mVideoView.setLayoutParams(new LayoutParams(width, height, left, top));
256        }
257    }
258
259    public void setImageVisibility(boolean visible) {
260        if (mImageView != null) {
261            if (mConformanceMode) {
262                mImageView.setVisibility(visible ? View.VISIBLE : View.GONE);
263            } else {
264                mImageView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
265            }
266        }
267    }
268
269    public void setTextVisibility(boolean visible) {
270        if (mScrollText != null) {
271            if (mConformanceMode) {
272                mTextView.setVisibility(visible ? View.VISIBLE : View.GONE);
273            } else {
274                mScrollText.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
275            }
276        }
277    }
278
279    public void setVideoVisibility(boolean visible) {
280        if (mVideoView != null) {
281            if (mConformanceMode) {
282                mVideoView.setVisibility(visible ? View.VISIBLE : View.GONE);
283            } else {
284                mVideoView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
285            }
286        }
287    }
288
289    public void startAudio() {
290        if ((mAudioPlayer != null) && mIsPrepared) {
291            mAudioPlayer.start();
292            mStartWhenPrepared = false;
293            displayAudioInfo();
294        } else {
295            mStartWhenPrepared = true;
296        }
297    }
298
299    public void stopAudio() {
300        if ((mAudioPlayer != null) && mIsPrepared) {
301            mAudioPlayer.stop();
302            mAudioPlayer.release();
303            mAudioPlayer = null;
304            hideAudioInfo();
305        } else {
306            mStopWhenPrepared = true;
307        }
308    }
309
310    public void pauseAudio() {
311        if ((mAudioPlayer != null) && mIsPrepared) {
312            if (mAudioPlayer.isPlaying()) {
313                mAudioPlayer.pause();
314            }
315        }
316        mStartWhenPrepared = false;
317    }
318
319    public void seekAudio(int seekTo) {
320        if ((mAudioPlayer != null) && mIsPrepared) {
321            mAudioPlayer.seekTo(seekTo);
322        } else {
323            mSeekWhenPrepared = seekTo;
324        }
325    }
326
327    public void startVideo() {
328        if (mVideoView != null) {
329            if (LOCAL_LOGV) {
330                Log.v(TAG, "Starting video playback.");
331            }
332            mVideoView.start();
333        }
334    }
335
336    public void stopVideo() {
337        if ((mVideoView != null)) {
338            if (LOCAL_LOGV) {
339                Log.v(TAG, "Stopping video playback.");
340            }
341            mVideoView.stopPlayback();
342        }
343    }
344
345    public void pauseVideo() {
346        if (mVideoView != null) {
347            if (LOCAL_LOGV) {
348                Log.v(TAG, "Pausing video playback.");
349            }
350            mVideoView.pause();
351        }
352    }
353
354    public void seekVideo(int seekTo) {
355        if (mVideoView != null) {
356            if (seekTo > 0) {
357                if (LOCAL_LOGV) {
358                    Log.v(TAG, "Seeking video playback to " + seekTo);
359                }
360                mVideoView.seekTo(seekTo);
361            }
362        }
363    }
364
365    public void reset() {
366        if (null != mScrollText) {
367            mScrollText.setVisibility(View.GONE);
368        }
369
370        if (null != mImageView) {
371            mImageView.setVisibility(View.GONE);
372        }
373
374        if (null != mAudioPlayer) {
375            stopAudio();
376        }
377
378        if (null != mVideoView) {
379            stopVideo();
380            mVideoView.setVisibility(View.GONE);
381        }
382
383        if (null != mTextView) {
384            mTextView.setVisibility(View.GONE);
385        }
386
387        if (mScrollViewPort != null) {
388            mScrollViewPort.scrollTo(0, 0);
389            mScrollViewPort.setLayoutParams(
390                    new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 0, 0));
391        }
392
393    }
394
395    public void setVisibility(boolean visible) {
396        // TODO Auto-generated method stub
397    }
398
399    @Override
400    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
401        super.onSizeChanged(w, h, oldw, oldh);
402
403        if (mSizeChangedListener != null) {
404            if (LOCAL_LOGV) {
405                Log.v(TAG, "new size=" + w + "x" + h);
406            }
407            mSizeChangedListener.onSizeChanged(w, h - AUDIO_INFO_HEIGHT);
408        }
409    }
410
411    public void setOnSizeChangedListener(OnSizeChangedListener l) {
412        mSizeChangedListener = l;
413    }
414
415    private class Position {
416        public Position(int left, int top) {
417            mTop = top;
418            mLeft = left;
419        }
420        public int mTop;
421        public int mLeft;
422    }
423
424    /**
425     * Makes the SlideView working on  MMSConformance Mode. The view will be
426     * re-layout to the linear view.
427     * <p>
428     * This is Chinese requirement about mms conformance.
429     * The most popular Mms service in China is newspaper which is MMS conformance,
430     * normally it mixes the image and text and has a number of slides. The
431     * AbsoluteLayout doesn't have good user experience for this kind of message,
432     * for example,
433     *
434     * 1. AbsoluteLayout exactly follows the smil's layout which is not optimized,
435     * and actually, no other MMS applications follow the smil's layout, they adjust
436     * the layout according their screen size. MMS conformance doc also allows the
437     * implementation to adjust the layout.
438     *
439     * 2. The TextView is fixed in the small area of screen, and other part of screen
440     * is empty once there is no image in the current slide.
441     *
442     * 3. The TextView is scrollable in a small area of screen and the font size is
443     * small which make the user experience bad.
444     *
445     * The better UI for the MMS conformance message could be putting the image/video
446     * and text in a linear layout view and making them scrollable together.
447     *
448     * Another reason for only applying the LinearLayout to the MMS conformance message
449     * is that the AbsoluteLayout has ability to play image and video in a same screen.
450     * which shouldn't be broken.
451     */
452    public void enableMMSConformanceMode(int textLeft, int textTop,
453            int imageLeft, int imageTop) {
454        mConformanceMode = true;
455        if (mScrollViewPort == null) {
456            mScrollViewPort = new ScrollView(mContext) {
457                private int mBottomY;
458                @Override
459                protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
460                    super.onLayout(changed, left, top, right, bottom);
461                    if (getChildCount() > 0) {
462                        int childHeight = getChildAt(0).getHeight();
463                        int height = getHeight();
464                        mBottomY = height < childHeight ? childHeight - height : 0;
465                    }
466                }
467                @Override
468                protected void onScrollChanged(int l, int t, int oldl, int oldt) {
469                    // Shows MediaController when the view is scrolled to the top/bottom of itself.
470                    if (t == 0 || t >= mBottomY){
471                        if (mMediaController != null) {
472                            mMediaController.show();
473                        }
474                    }
475                }
476            };
477            mScrollViewPort.setScrollBarStyle(SCROLLBARS_OUTSIDE_INSET);
478            mViewPort = new LinearLayout(mContext);
479            mViewPort.setOrientation(LinearLayout.VERTICAL);
480            mViewPort.setGravity(Gravity.CENTER);
481            mViewPort.setOnClickListener(new OnClickListener() {
482                public void onClick(View v) {
483                    if (mMediaController != null) {
484                        mMediaController.show();
485                    }
486                }
487            });
488            mScrollViewPort.addView(mViewPort, new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
489                    LayoutParams.WRAP_CONTENT));
490            addView(mScrollViewPort);
491        }
492        // Layout views to fit the LinearLayout from left to right, then top to
493        // bottom.
494        TreeMap<Position, View> viewsByPosition = new TreeMap<Position, View>(new Comparator<Position>() {
495            public int compare(Position p1, Position p2) {
496                int l1 = p1.mLeft;
497                int t1 = p1.mTop;
498                int l2 = p2.mLeft;
499                int t2 = p2.mTop;
500                int res = t1 - t2;
501                if (res == 0) {
502                    res = l1 - l2;
503                }
504                if (res == 0) {
505                    // A view will be lost if return 0.
506                    return -1;
507                }
508                return res;
509            }
510        });
511        if (textLeft >=0 && textTop >=0) {
512            mTextView = new TextView(mContext);
513            mTextView.setTransformationMethod(HideReturnsTransformationMethod.getInstance());
514            mTextView.setTextSize(18);
515            mTextView.setPadding(5, 5, 5, 5);
516            viewsByPosition.put(new Position(textLeft, textTop), mTextView);
517        }
518
519        if (imageLeft >=0 && imageTop >=0) {
520            mImageView = new ImageView(mContext);
521            mImageView.setPadding(0, 5, 0, 5);
522            viewsByPosition.put(new Position(imageLeft, imageTop), mImageView);
523            // According MMS Conformance Document, the image and video should use the same
524            // region. So, put the VideoView below the ImageView.
525            mVideoView = new VideoView(mContext);
526            viewsByPosition.put(new Position(imageLeft + 1, imageTop), mVideoView);
527        }
528        for (View view : viewsByPosition.values()) {
529            if (view instanceof VideoView) {
530                mViewPort.addView(view, new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT,
531                        LayoutManager.getInstance().getLayoutParameters().getHeight()));
532            } else {
533                mViewPort.addView(view, new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT,
534                        LayoutParams.WRAP_CONTENT));
535            }
536            view.setVisibility(View.GONE);
537        }
538    }
539}
540