diff --git a/samples/ApiDemos/src/com/example/android/apis/view/VideoPlayerActivity.java b/samples/ApiDemos/src/com/example/android/apis/view/VideoPlayerActivity.java index e5333d705..3bee59460 100644 --- a/samples/ApiDemos/src/com/example/android/apis/view/VideoPlayerActivity.java +++ b/samples/ApiDemos/src/com/example/android/apis/view/VideoPlayerActivity.java @@ -167,7 +167,7 @@ public class VideoPlayerActivity extends Activity h.removeCallbacks(mNavHider); if (!mMenusOpen && !mPaused) { // If the menus are open or play is paused, we will not auto-hide. - h.postDelayed(mNavHider, 1500); + h.postDelayed(mNavHider, 3000); } } } diff --git a/samples/Support4Demos/res/layout/media_controller.xml b/samples/Support4Demos/res/layout/media_controller.xml new file mode 100644 index 000000000..b5e58b184 --- /dev/null +++ b/samples/Support4Demos/res/layout/media_controller.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Support4Demos/res/layout/videoview.xml b/samples/Support4Demos/res/layout/videoview.xml index ff636b99a..abc1f7e51 100644 --- a/samples/Support4Demos/res/layout/videoview.xml +++ b/samples/Support4Demos/res/layout/videoview.xml @@ -1,14 +1,39 @@ + + - - - + /> + + + + diff --git a/samples/Support4Demos/src/com/example/android/supportv4/media/MediaController.java b/samples/Support4Demos/src/com/example/android/supportv4/media/MediaController.java new file mode 100644 index 000000000..e7f9e080b --- /dev/null +++ b/samples/Support4Demos/src/com/example/android/supportv4/media/MediaController.java @@ -0,0 +1,360 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.supportv4.media; + +import com.example.android.supportv4.R; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.ProgressBar; +import android.widget.SeekBar; +import android.widget.TextView; + +import java.util.Formatter; +import java.util.Locale; + +/** + * Helper for implementing media controls in an application. + * Use instead of the very useful android.widget.MediaController. + * This version is embedded inside of an application's layout. + */ +public class MediaController extends FrameLayout { + + private MediaPlayerControl mPlayer; + private Context mContext; + private ProgressBar mProgress; + private TextView mEndTime, mCurrentTime; + private boolean mDragging; + private boolean mUseFastForward; + private boolean mFromXml; + private boolean mListenersSet; + private View.OnClickListener mNextListener, mPrevListener; + StringBuilder mFormatBuilder; + Formatter mFormatter; + private ImageButton mPauseButton; + private ImageButton mFfwdButton; + private ImageButton mRewButton; + private ImageButton mNextButton; + private ImageButton mPrevButton; + + public MediaController(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + mUseFastForward = true; + mFromXml = true; + LayoutInflater inflate = (LayoutInflater) + mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflate.inflate(R.layout.media_controller, this, true); + initControllerView(); + } + + public MediaController(Context context, boolean useFastForward) { + super(context); + mContext = context; + mUseFastForward = useFastForward; + } + + public MediaController(Context context) { + this(context, true); + } + + public void setMediaPlayer(MediaPlayerControl player) { + mPlayer = player; + updatePausePlay(); + } + + private void initControllerView() { + mPauseButton = (ImageButton) findViewById(R.id.pause); + if (mPauseButton != null) { + mPauseButton.requestFocus(); + mPauseButton.setOnClickListener(mPauseListener); + } + + mFfwdButton = (ImageButton) findViewById(R.id.ffwd); + if (mFfwdButton != null) { + mFfwdButton.setOnClickListener(mFfwdListener); + if (!mFromXml) { + mFfwdButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE); + } + } + + mRewButton = (ImageButton) findViewById(R.id.rew); + if (mRewButton != null) { + mRewButton.setOnClickListener(mRewListener); + if (!mFromXml) { + mRewButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE); + } + } + + // By default these are hidden. They will be enabled when setPrevNextListeners() is called + mNextButton = (ImageButton) findViewById(R.id.next); + if (mNextButton != null && !mFromXml && !mListenersSet) { + mNextButton.setVisibility(View.GONE); + } + mPrevButton = (ImageButton) findViewById(R.id.prev); + if (mPrevButton != null && !mFromXml && !mListenersSet) { + mPrevButton.setVisibility(View.GONE); + } + + mProgress = (ProgressBar) findViewById(R.id.mediacontroller_progress); + if (mProgress != null) { + if (mProgress instanceof SeekBar) { + SeekBar seeker = (SeekBar) mProgress; + seeker.setOnSeekBarChangeListener(mSeekListener); + } + mProgress.setMax(1000); + } + + mEndTime = (TextView) findViewById(R.id.time); + mCurrentTime = (TextView) findViewById(R.id.time_current); + mFormatBuilder = new StringBuilder(); + mFormatter = new Formatter(mFormatBuilder, Locale.getDefault()); + + installPrevNextListeners(); + } + + /** + * Disable pause or seek buttons if the stream cannot be paused or seeked. + * This requires the control interface to be a MediaPlayerControlExt + */ + private void disableUnsupportedButtons() { + try { + if (mPauseButton != null && !mPlayer.canPause()) { + mPauseButton.setEnabled(false); + } + if (mRewButton != null && !mPlayer.canSeekBackward()) { + mRewButton.setEnabled(false); + } + if (mFfwdButton != null && !mPlayer.canSeekForward()) { + mFfwdButton.setEnabled(false); + } + } catch (IncompatibleClassChangeError ex) { + // We were given an old version of the interface, that doesn't have + // the canPause/canSeekXYZ methods. This is OK, it just means we + // assume the media can be paused and seeked, and so we don't disable + // the buttons. + } + } + + public void refresh() { + updateProgress(); + disableUnsupportedButtons(); + updatePausePlay(); + } + + private String stringForTime(int timeMs) { + int totalSeconds = timeMs / 1000; + + int seconds = totalSeconds % 60; + int minutes = (totalSeconds / 60) % 60; + int hours = totalSeconds / 3600; + + mFormatBuilder.setLength(0); + if (hours > 0) { + return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString(); + } else { + return mFormatter.format("%02d:%02d", minutes, seconds).toString(); + } + } + + public int updateProgress() { + if (mPlayer == null || mDragging) { + return 0; + } + int position = mPlayer.getCurrentPosition(); + int duration = mPlayer.getDuration(); + if (mProgress != null) { + if (duration > 0) { + // use long to avoid overflow + long pos = 1000L * position / duration; + mProgress.setProgress( (int) pos); + } + int percent = mPlayer.getBufferPercentage(); + mProgress.setSecondaryProgress(percent * 10); + } + + if (mEndTime != null) + mEndTime.setText(stringForTime(duration)); + if (mCurrentTime != null) + mCurrentTime.setText(stringForTime(position)); + + return position; + } + + private View.OnClickListener mPauseListener = new View.OnClickListener() { + public void onClick(View v) { + doPauseResume(); + } + }; + + private void updatePausePlay() { + if (mPauseButton == null) + return; + + if (mPlayer.isPlaying()) { + mPauseButton.setImageResource(android.R.drawable.ic_media_pause); + } else { + mPauseButton.setImageResource(android.R.drawable.ic_media_play); + } + } + + private void doPauseResume() { + if (mPlayer.isPlaying()) { + mPlayer.pause(); + } else { + mPlayer.start(); + } + updatePausePlay(); + } + + // There are two scenarios that can trigger the seekbar listener to trigger: + // + // The first is the user using the touchpad to adjust the posititon of the + // seekbar's thumb. In this case onStartTrackingTouch is called followed by + // a number of onProgressChanged notifications, concluded by onStopTrackingTouch. + // We're setting the field "mDragging" to true for the duration of the dragging + // session to avoid jumps in the position in case of ongoing playback. + // + // The second scenario involves the user operating the scroll ball, in this + // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications, + // we will simply apply the updated position without suspending regular updates. + private SeekBar.OnSeekBarChangeListener mSeekListener = new SeekBar.OnSeekBarChangeListener() { + public void onStartTrackingTouch(SeekBar bar) { + mDragging = true; + } + + public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) { + if (!fromuser) { + // We're not interested in programmatically generated changes to + // the progress bar's position. + return; + } + + long duration = mPlayer.getDuration(); + long newposition = (duration * progress) / 1000L; + mPlayer.seekTo( (int) newposition); + if (mCurrentTime != null) + mCurrentTime.setText(stringForTime( (int) newposition)); + } + + public void onStopTrackingTouch(SeekBar bar) { + mDragging = false; + updateProgress(); + updatePausePlay(); + } + }; + + @Override + public void setEnabled(boolean enabled) { + if (mPauseButton != null) { + mPauseButton.setEnabled(enabled); + } + if (mFfwdButton != null) { + mFfwdButton.setEnabled(enabled); + } + if (mRewButton != null) { + mRewButton.setEnabled(enabled); + } + if (mNextButton != null) { + mNextButton.setEnabled(enabled && mNextListener != null); + } + if (mPrevButton != null) { + mPrevButton.setEnabled(enabled && mPrevListener != null); + } + if (mProgress != null) { + mProgress.setEnabled(enabled); + } + disableUnsupportedButtons(); + super.setEnabled(enabled); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(MediaController.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(MediaController.class.getName()); + } + + private View.OnClickListener mRewListener = new View.OnClickListener() { + public void onClick(View v) { + int pos = mPlayer.getCurrentPosition(); + pos -= 5000; // milliseconds + mPlayer.seekTo(pos); + updateProgress(); + } + }; + + private View.OnClickListener mFfwdListener = new View.OnClickListener() { + public void onClick(View v) { + int pos = mPlayer.getCurrentPosition(); + pos += 15000; // milliseconds + mPlayer.seekTo(pos); + updateProgress(); + } + }; + + private void installPrevNextListeners() { + if (mNextButton != null) { + mNextButton.setOnClickListener(mNextListener); + mNextButton.setEnabled(mNextListener != null); + } + + if (mPrevButton != null) { + mPrevButton.setOnClickListener(mPrevListener); + mPrevButton.setEnabled(mPrevListener != null); + } + } + + public void setPrevNextListeners(View.OnClickListener next, View.OnClickListener prev) { + mNextListener = next; + mPrevListener = prev; + mListenersSet = true; + + installPrevNextListeners(); + + if (mNextButton != null && !mFromXml) { + mNextButton.setVisibility(View.VISIBLE); + } + if (mPrevButton != null && !mFromXml) { + mPrevButton.setVisibility(View.VISIBLE); + } + } + + public interface MediaPlayerControl { + void start(); + void pause(); + int getDuration(); + int getCurrentPosition(); + void seekTo(int pos); + boolean isPlaying(); + int getBufferPercentage(); + boolean canPause(); + boolean canSeekBackward(); + boolean canSeekForward(); + } +} diff --git a/samples/Support4Demos/src/com/example/android/supportv4/media/TransportControllerActivity.java b/samples/Support4Demos/src/com/example/android/supportv4/media/TransportControllerActivity.java index e6d28a766..a4e0951ce 100644 --- a/samples/Support4Demos/src/com/example/android/supportv4/media/TransportControllerActivity.java +++ b/samples/Support4Demos/src/com/example/android/supportv4/media/TransportControllerActivity.java @@ -16,13 +16,19 @@ package com.example.android.supportv4.media; -import android.view.KeyEvent; import com.example.android.supportv4.R; +import android.app.ActionBar; +import android.content.Context; +import android.media.MediaPlayer; +import android.os.Handler; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.View; + import android.app.Activity; import android.net.Uri; import android.os.Bundle; -import android.widget.MediaController; import android.widget.VideoView; import android.support.v4.media.TransportController; @@ -33,33 +39,266 @@ public class TransportControllerActivity extends Activity { * TODO: Set the path variable to a streaming video URL or a local media * file path. */ - private VideoView mVideoView; + private Content mContent; private TransportController mTransportController; + private MediaController mMediaController; /** * Handle media buttons to start/stop video playback. Real implementations * will probably handle more buttons, like skip and fast-forward. */ - public class PlayerControlCallbacks extends TransportController.Callbacks { + TransportController.Callbacks mTransportCallbacks = new TransportController.Callbacks() { public boolean onMediaButtonDown(int keyCode, KeyEvent event) { switch (keyCode) { case TransportController.KEYCODE_MEDIA_PLAY: - mVideoView.start(); + mMediaPlayerControl.start(); return true; case TransportController.KEYCODE_MEDIA_PAUSE: case KeyEvent.KEYCODE_MEDIA_STOP: - mVideoView.pause(); + mMediaPlayerControl.pause(); return true; case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: case KeyEvent.KEYCODE_HEADSETHOOK: - if (mVideoView.isPlaying()) { - mVideoView.pause(); + if (mContent.isPlaying()) { + mMediaPlayerControl.pause(); } else { - mVideoView.start(); + mMediaPlayerControl.start(); } } return true; } + }; + + /** + * Handle actions from on-screen media controls. Most of these are simple re-directs + * to the VideoView; some we need to capture to update our state. + */ + MediaController.MediaPlayerControl mMediaPlayerControl + = new MediaController.MediaPlayerControl() { + + @Override + public void start() { + mTransportController.startPlaying(); + mContent.start(); + } + + @Override + public void pause() { + mTransportController.pausePlaying(); + mContent.pause(); + } + + @Override + public int getDuration() { + return mContent.getDuration(); + } + + @Override + public int getCurrentPosition() { + return mContent.getCurrentPosition(); + } + + @Override + public void seekTo(int pos) { + mContent.seekTo(pos); + } + + @Override + public boolean isPlaying() { + return mContent.isPlaying(); + } + + @Override + public int getBufferPercentage() { + return mContent.getBufferPercentage(); + } + + @Override + public boolean canPause() { + return mContent.canPause(); + } + + @Override + public boolean canSeekBackward() { + return mContent.canSeekBackward(); + } + + @Override + public boolean canSeekForward() { + return mContent.canSeekForward(); + } + }; + + /** + * This is the actual video player. It is the top-level content of + * the activity's view hierarchy, going under the status bar and nav + * bar areas. + */ + public static class Content extends VideoView implements + View.OnSystemUiVisibilityChangeListener, View.OnClickListener, + ActionBar.OnMenuVisibilityListener, MediaPlayer.OnPreparedListener, + MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener { + Activity mActivity; + TransportController mTransportController; + MediaController mMediaController; + boolean mAddedMenuListener; + boolean mMenusOpen; + boolean mPaused; + boolean mNavVisible; + int mLastSystemUiVis; + + Runnable mNavHider = new Runnable() { + @Override public void run() { + setNavVisibility(false); + } + }; + + Runnable mProgressUpdater = new Runnable() { + @Override public void run() { + mMediaController.updateProgress(); + getHandler().postDelayed(this, 1000); + } + }; + + public Content(Context context, AttributeSet attrs) { + super(context, attrs); + setOnSystemUiVisibilityChangeListener(this); + setOnClickListener(this); + setOnPreparedListener(this); + setOnCompletionListener(this); + setOnErrorListener(this); + } + + public void init(Activity activity, TransportController transportController, + MediaController mediaController) { + // This called by the containing activity to supply the surrounding + // state of the video player that it will interact with. + mActivity = activity; + mTransportController = transportController; + mMediaController = mediaController; + pause(); + } + + @Override protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (mActivity != null) { + mAddedMenuListener = true; + mActivity.getActionBar().addOnMenuVisibilityListener(this); + } + } + + @Override protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mAddedMenuListener) { + mActivity.getActionBar().removeOnMenuVisibilityListener(this); + } + mNavVisible = false; + } + + @Override public void onSystemUiVisibilityChange(int visibility) { + // Detect when we go out of nav-hidden mode, to clear our state + // back to having the full UI chrome up. Only do this when + // the state is changing and nav is no longer hidden. + int diff = mLastSystemUiVis ^ visibility; + mLastSystemUiVis = visibility; + if ((diff&SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0 + && (visibility&SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) { + setNavVisibility(true); + } + } + + @Override protected void onWindowVisibilityChanged(int visibility) { + super.onWindowVisibilityChanged(visibility); + + // When we become visible or invisible, play is paused. + pause(); + } + + @Override public void onClick(View v) { + // Clicking anywhere makes the navigation visible. + setNavVisibility(true); + } + + @Override public void onMenuVisibilityChanged(boolean isVisible) { + mMenusOpen = isVisible; + setNavVisibility(true); + } + + @Override + public void onPrepared(MediaPlayer mp) { + mMediaController.setEnabled(true); + } + + @Override + public void onCompletion(MediaPlayer mp) { + mTransportController.pausePlaying(); + pause(); + } + + @Override + public boolean onError(MediaPlayer mp, int what, int extra) { + mTransportController.pausePlaying(); + pause(); + return false; + } + + @Override public void start() { + super.start(); + mPaused = false; + setKeepScreenOn(true); + setNavVisibility(true); + mMediaController.refresh(); + scheduleProgressUpdater(); + } + + @Override public void pause() { + super.pause(); + mPaused = true; + setKeepScreenOn(false); + setNavVisibility(true); + mMediaController.refresh(); + scheduleProgressUpdater(); + } + + void scheduleProgressUpdater() { + Handler h = getHandler(); + if (h != null) { + if (mNavVisible && !mPaused) { + h.removeCallbacks(mProgressUpdater); + h.post(mProgressUpdater); + } else { + h.removeCallbacks(mProgressUpdater); + } + } + } + + void setNavVisibility(boolean visible) { + int newVis = SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | SYSTEM_UI_FLAG_LAYOUT_STABLE; + if (!visible) { + newVis |= SYSTEM_UI_FLAG_LOW_PROFILE | SYSTEM_UI_FLAG_FULLSCREEN + | SYSTEM_UI_FLAG_HIDE_NAVIGATION; + } + + // If we are now visible, schedule a timer for us to go invisible. + if (visible) { + Handler h = getHandler(); + if (h != null) { + h.removeCallbacks(mNavHider); + if (!mMenusOpen && !mPaused) { + // If the menus are open or play is paused, we will not auto-hide. + h.postDelayed(mNavHider, 3000); + } + } + } + + // Set the new desired visibility. + setSystemUiVisibility(newVis); + mNavVisible = visible; + mMediaController.setVisibility(visible ? VISIBLE : INVISIBLE); + scheduleProgressUpdater(); + } } @Override @@ -68,27 +307,31 @@ public class TransportControllerActivity extends Activity { setContentView(R.layout.videoview); // Find the video player in our UI. - mVideoView = (VideoView) findViewById(R.id.surface_view); + mContent = (Content) findViewById(R.id.content); - // Create transport controller to control video; use the standard - // control callbacks that knows how to talk to a MediaPlayerControl. - mTransportController = new TransportController(this, new PlayerControlCallbacks()); + // Create and initialize the media control UI. + mMediaController = (MediaController) findViewById(R.id.media_controller); + mMediaController.setMediaPlayer(mMediaPlayerControl); + + // Create transport controller to control video, giving the callback + // interface to receive actions from. + mTransportController = new TransportController(this, mTransportCallbacks); // We're just playing a built-in demo video. - mVideoView.setVideoURI(Uri.parse("android.resource://" + getPackageName() + + mContent.init(this, mTransportController, mMediaController); + mContent.setVideoURI(Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.videoviewdemo)); - mVideoView.setMediaController(new MediaController(this)); - mVideoView.requestFocus(); } @Override public boolean dispatchKeyEvent(KeyEvent event) { - if (super.dispatchKeyEvent(event)) { + // We first dispatch keys to the transport controller -- we want it + // to get to consume any media keys rather than letting whoever has focus + // in the view hierarchy to potentially eat it. + if (mTransportController.dispatchKeyEvent(event)) { return true; } - // If the UI didn't handle the key, give the transport controller - // a crack at it. - return mTransportController.dispatchKeyEvent(event); + return super.dispatchKeyEvent(event); } }