/* * Copyright (C) 2011 VOV IO (http://vov.io/) * * Based on the code from the Android Open Player * http://code.google.com/p/android-oplayer/ */ /** * The MIT License (MIT) * * Copyright (c) 2012-2013 David Carver * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS * OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF * OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package us.nineworlds.serenity.ui.video.player; import java.text.SimpleDateFormat; import java.util.Date; import java.util.LinkedList; import java.util.Locale; import javax.inject.Inject; import us.nineworlds.serenity.R; import us.nineworlds.serenity.core.imageloader.SerenityImageLoader; import us.nineworlds.serenity.core.model.VideoContentInfo; import us.nineworlds.serenity.core.util.AndroidHelper; import us.nineworlds.serenity.core.util.TimeUtil; import us.nineworlds.serenity.injection.ForVideoQueue; import us.nineworlds.serenity.injection.SerenityObjectGraph; import us.nineworlds.serenity.ui.util.ImageInfographicUtils; import android.app.Activity; import android.content.Context; import android.media.AudioManager; import android.os.Handler; import android.os.Message; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.PopupWindow; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; /** * Based on the Android Open Source MediaController but drastically refactored * and cleaned up. * * DAC - 2013-03-28 - Cleaned up the code and removed the handling of the key * events. Also made it so that the mediacontroller popup windows is focusable * otherwise no keyevents were being fired. * * DAC - 2014-05-20 - Fix issue on Android 4.3 or higher were mediacontroller * window position was not being calculated correctly. * */ public class MediaController extends FrameLayout { private static final int FADE_OUT = 1; private static final int SHOW_PROGRESS = 2; private int defaultTimeOut = 5000; private AudioManager audioManager; private View anchorView; private int animationStyle; private Context context; private boolean draggingSeeker; private long playbackDuration; private MediaPlayerControl mediaPlayerControl; private TextView endTimeTextView, currentTimeTextView; private boolean fromLayoutXML = false; private final Handler showfadeMessgeHandler = new ShowMessageHandler(); private OnHiddenListener onHideListener; private boolean instantSeeking = true; private ImageButton pauseImageButton; private SeekBar progressSeekBar; private View rootView; private final OnSeekBarChangeListener mSeekListener = new SeekOnSeekBarChangeListener(); private boolean mediaControllerShowing; private OnShownListener onShownListener; private PopupWindow mediaControllerHUD; private MediaControllerDataObject mediaMetaData; @Inject @ForVideoQueue protected LinkedList<VideoContentInfo> videoQueue; @Inject protected SerenityImageLoader serenityImageLoader; @Inject protected AndroidHelper androidHelper; @Inject protected TimeUtil timeUtil; @Deprecated public MediaController(Context context, AttributeSet attrs) { super(context, attrs); SerenityObjectGraph.getInstance().inject(this); rootView = this; fromLayoutXML = true; initController(context); } public void setOSDDelayTime(int delayTime) { defaultTimeOut = delayTime; } public MediaController(MediaControllerDataObject mediaMetaData) { super(mediaMetaData.getContext()); SerenityObjectGraph.getInstance().inject(this); this.mediaMetaData = mediaMetaData; if (!fromLayoutXML && initController(mediaMetaData.getContext())) { initFloatingWindow(); } } protected void createDurationViews(View v) { endTimeTextView = (TextView) v .findViewById(R.id.mediacontroller_time_total); currentTimeTextView = (TextView) v .findViewById(R.id.mediacontroller_time_current); } protected void createNextVideoButton(View v) { TextView txtNextVideo = (TextView) v .findViewById(R.id.mediacontroller_next_video); if (txtNextVideo == null) { return; } if (videoQueue.isEmpty()) { txtNextVideo.setVisibility(View.GONE); return; } VideoContentInfo nextVideo = videoQueue.peek(); txtNextVideo.setText(getResources().getString( R.string.mediacontroller_on_deck) + nextVideo.getTitle()); txtNextVideo.setVisibility(View.VISIBLE); } protected void createPauseButton(View v) { pauseImageButton = (ImageButton) v .findViewById(R.id.mediacontroller_play_pause); if (pauseImageButton != null) { pauseImageButton.requestFocus(); pauseImageButton.setOnClickListener(new PauseOnClickListener(this)); } } protected void createPoster(View v) { ImageView posterView = (ImageView) v .findViewById(R.id.mediacontroller_poster_art); posterView.setScaleType(ImageView.ScaleType.FIT_CENTER); if (mediaMetaData.getPosterURL() != null) { serenityImageLoader.displayImage(mediaMetaData.getPosterURL(), posterView); } } protected void createProgressBar(View v) { progressSeekBar = (SeekBar) v .findViewById(R.id.mediacontroller_seekbar); if (progressSeekBar != null) { if (progressSeekBar instanceof SeekBar) { SeekBar seeker = progressSeekBar; seeker.setOnSeekBarChangeListener(mSeekListener); seeker.setThumbOffset(1); } progressSeekBar.setMax(1000); } } protected void createSkipBackwardsButton(View v) { ImageButton skipBackwardButton = (ImageButton) v .findViewById(R.id.osd_rewind_control); if (skipBackwardButton != null) { skipBackwardButton .setOnClickListener(new SkipBackwardOnClickListener(this)); } } protected void createSkipForwardButton(View v) { ImageButton skipForwardButton = (ImageButton) v .findViewById(R.id.osd_ff_control); if (skipForwardButton != null) { skipForwardButton .setOnClickListener(new SkipForwardOnClickListener(this)); } } private void disableUnsupportedButtons() { try { if (pauseImageButton != null && !mediaPlayerControl.canPause()) { pauseImageButton.setEnabled(false); } } catch (IncompatibleClassChangeError ex) { } } @Override public boolean dispatchKeyEvent(KeyEvent event) { int keyCode = event.getKeyCode(); Activity c = (Activity) getContext(); if (keyCode == KeyEvent.KEYCODE_BACK) { c.onKeyDown(keyCode, event); return true; } // return c.dispatchKeyEvent(event); boolean superHandled = super.dispatchKeyEvent(event); if (superHandled) { return true; } return c.dispatchKeyEvent(event); } public MediaPlayerControl getMediaPlayerControl() { return mediaPlayerControl; } public void hide() { if (anchorView == null) { return; } if (!mediaControllerShowing) { return; } try { showfadeMessgeHandler.removeMessages(SHOW_PROGRESS); if (fromLayoutXML) { setVisibility(View.GONE); } else { mediaControllerHUD.dismiss(); } anchorView.setSystemUiVisibility(View.STATUS_BAR_HIDDEN); } catch (IllegalArgumentException ex) { Log.d("SerentityMediaController", "MediaController already removed", ex); } mediaControllerShowing = false; if (onHideListener != null) { onHideListener.onHidden(); } } private boolean initController(Context context) { this.context = context; audioManager = (AudioManager) context .getSystemService(Context.AUDIO_SERVICE); return true; } protected void initControllerView(View v) { createPauseButton(v); createSkipBackwardsButton(v); createSkipForwardButton(v); createProgressBar(v); createDurationViews(v); createPoster(v); initTitle(v); initSummary(v); initVideoMetaData(v); createNextVideoButton(v); } private void initFloatingWindow() { mediaControllerHUD = new PopupWindow(context); mediaControllerHUD.setFocusable(true); mediaControllerHUD.setBackgroundDrawable(null); mediaControllerHUD.setOutsideTouchable(true); mediaControllerHUD.update(); animationStyle = R.style.PopupAnimation; setFocusable(true); setFocusableInTouchMode(true); requestFocus(); } protected void initSummary(View v) { TextView summaryView = (TextView) v .findViewById(R.id.mediacontroller_summary); summaryView.setText(mediaMetaData.getSummary()); } protected void initTitle(View v) { TextView textTitle = (TextView) v .findViewById(R.id.mediacontroller_title); textTitle.setText(mediaMetaData.getTitle()); } /** * @param v */ protected void initVideoMetaData(View v) { LinearLayout infoGraphic = (LinearLayout) v .findViewById(R.id.mediacontroller_infographic_layout); ImageInfographicUtils iiu = new ImageInfographicUtils(75, 70); if (mediaMetaData.getResolution() != null) { ImageView rv = iiu.createVideoResolutionImage( mediaMetaData.getResolution(), v.getContext()); if (rv != null) { infoGraphic.addView(rv); } } if (mediaMetaData.getVideoFormat() != null) { ImageView vr = iiu.createVideoCodec(mediaMetaData.getVideoFormat(), v.getContext()); if (vr != null) { infoGraphic.addView(vr); } } if (mediaMetaData.getAudioFormat() != null) { ImageView ar = iiu.createAudioCodecImage( mediaMetaData.getAudioFormat(), v.getContext()); if (ar != null) { infoGraphic.addView(ar); } } if (mediaMetaData.getAudioChannels() != null) { ImageView ar = iiu.createAudioChannlesImage( mediaMetaData.getAudioChannels(), v.getContext()); if (ar != null) { infoGraphic.addView(ar); } } } public boolean isShowing() { return mediaControllerShowing; } /** * Create the view that holds the widgets that control playback. Derived * classes can override this to create their own. * * @return The controller view. */ protected View makeControllerView() { if (androidHelper.isRunningOnOUYA()) { return View.inflate(context, R.layout.serenity_media_controller_ouya, this); } return View.inflate(context, R.layout.serenity_media_controller, this); } @Override public void onFinishInflate() { if (rootView != null) { initControllerView(rootView); } } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { Activity c = (Activity) getContext(); return c.onKeyDown(keyCode, event); } @Override public boolean onTouchEvent(MotionEvent event) { show(defaultTimeOut); return true; } @Override public boolean onTrackballEvent(MotionEvent ev) { show(defaultTimeOut); return false; } /** * */ protected void positionPopupWindow() { int[] location = new int[2]; anchorView.getLocationOnScreen(location); rootView.measure(MeasureSpec.makeMeasureSpec(anchorView.getWidth(), MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec( anchorView.getHeight(), MeasureSpec.AT_MOST)); int x = location[0] / 2; int y = location[1] + anchorView.getHeight() - rootView.getMeasuredHeight(); mediaControllerHUD.showAtLocation(anchorView, Gravity.NO_GRAVITY, x, y); mediaControllerHUD.setAnimationStyle(animationStyle); } /** * Set the view that acts as the anchor for the control view. This can for * example be a VideoView, or your Activity's main view. * * @param view * The view to which to anchor the controller when it is visible. */ public void setAnchorView(View view) { anchorView = view; if (!fromLayoutXML) { removeAllViews(); rootView = makeControllerView(); mediaControllerHUD.setContentView(rootView); mediaControllerHUD.setWidth(ViewGroup.LayoutParams.MATCH_PARENT); mediaControllerHUD .setHeight(android.view.ViewGroup.LayoutParams.MATCH_PARENT); } initControllerView(rootView); } /** * <p> * Change the animation style resource for this controller. * </p> * * <p> * If the controller is showing, calling this method will take effect only * the next time the controller is shown. * </p> * * @param animationStyle * animation style to use when the controller appears and * disappears. Set to -1 for the default animation, 0 for no * animation, or a resource identifier for an explicit animation. * */ public void setAnimationStyle(int animationStyle) { this.animationStyle = animationStyle; } @Override public void setEnabled(boolean enabled) { if (pauseImageButton != null) { pauseImageButton.setEnabled(enabled); } if (progressSeekBar != null) { progressSeekBar.setEnabled(enabled); } disableUnsupportedButtons(); super.setEnabled(enabled); } /** * Control the action when the seekbar dragged by user * * @param seekWhenDragging * True the media will seek periodically */ public void setInstantSeeking(boolean seekWhenDragging) { instantSeeking = seekWhenDragging; } public void setMediaPlayer(MediaPlayerControl player) { mediaPlayerControl = player; } /** * @param mPlayer * the mPlayer to set */ public void setMediaPlayerControl(MediaPlayerControl mPlayer) { this.mediaPlayerControl = mPlayer; } public void setOnHiddenListener(OnHiddenListener l) { onHideListener = l; } public void setOnShownListener(OnShownListener l) { onShownListener = l; } private long setProgress() { if (mediaPlayerControl == null || draggingSeeker) { return 0; } long position = 0; try { position = mediaPlayerControl.getCurrentPosition(); long duration = mediaPlayerControl.getDuration(); if (progressSeekBar != null) { if (duration > 0) { long pos = 1000L * position / duration; progressSeekBar.setProgress((int) pos); } int percent = mediaPlayerControl.getBufferPercentage(); progressSeekBar.setSecondaryProgress(percent * 10); } playbackDuration = duration; if (endTimeTextView != null) { endTimeTextView.setText(timeUtil .formatDuration(playbackDuration)); } if (currentTimeTextView != null) { currentTimeTextView.setText(timeUtil.formatDuration(position)); } } catch (IllegalStateException ex) { Log.i(getClass().getName(), "Player has been either released or in an error state."); } return position; } public void show() { show(defaultTimeOut); } /** * Show the controller on screen. It will go away automatically after * 'timeout' milliseconds of inactivity. * * @param timeout * The timeout in milliseconds. Use 0 to show the controller * until hide() is called. */ public void show(int timeout) { if (!mediaControllerShowing && anchorView != null && anchorView.getWindowToken() != null) { if (pauseImageButton != null) { pauseImageButton.requestFocus(); } disableUnsupportedButtons(); if (fromLayoutXML) { setVisibility(View.VISIBLE); } else { positionPopupWindow(); } anchorView.setSystemUiVisibility(View.STATUS_BAR_VISIBLE); mediaControllerShowing = true; if (onShownListener != null) { onShownListener.onShown(); } } showfadeMessgeHandler.sendEmptyMessage(SHOW_PROGRESS); if (timeout != 0) { showfadeMessgeHandler.removeMessages(FADE_OUT); showfadeMessgeHandler.sendMessageDelayed( showfadeMessgeHandler.obtainMessage(FADE_OUT), timeout); } } public interface OnHiddenListener { public void onHidden(); } public interface OnShownListener { public void onShown(); } protected class SeekOnSeekBarChangeListener implements OnSeekBarChangeListener { @Override public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) { try { if (!fromuser) { return; } long newposition = (playbackDuration * progress) / 1000; String time = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(new Date(newposition)); if (instantSeeking) { mediaPlayerControl.seekTo(newposition); } if (currentTimeTextView != null) { currentTimeTextView.setText(time); } } catch (IllegalStateException e) { Log.d(getClass().getName(), "Seeking failed due to media player in an illegalstate.", e); } } @Override public void onStartTrackingTouch(SeekBar bar) { draggingSeeker = true; show(3600000); showfadeMessgeHandler.removeMessages(SHOW_PROGRESS); if (instantSeeking) { audioManager.setStreamMute(AudioManager.STREAM_MUSIC, true); } } @Override public void onStopTrackingTouch(SeekBar bar) { try { if (!instantSeeking) { mediaPlayerControl.seekTo((playbackDuration * bar .getProgress()) / 1000); } show(defaultTimeOut); showfadeMessgeHandler.removeMessages(SHOW_PROGRESS); audioManager.setStreamMute(AudioManager.STREAM_MUSIC, false); draggingSeeker = false; showfadeMessgeHandler.sendEmptyMessageDelayed(SHOW_PROGRESS, 1000); } catch (IllegalStateException e) { Log.d(getClass().getName(), "Seeking failed due to media player in an illegalstate.", e); } } } protected class ShowMessageHandler extends Handler { @Override public void handleMessage(Message msg) { long pos; switch (msg.what) { case FADE_OUT: hide(); break; case SHOW_PROGRESS: if (!draggingSeeker && mediaControllerShowing) { pos = setProgress(); msg = obtainMessage(SHOW_PROGRESS); sendMessageDelayed(msg, 1000 - (pos % 1000)); } break; } } } }