1/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.widget;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.graphics.PixelFormat;
22import android.media.AudioManager;
23import android.os.Handler;
24import android.os.Message;
25import android.util.AttributeSet;
26import android.util.Log;
27import android.view.Gravity;
28import android.view.KeyEvent;
29import android.view.LayoutInflater;
30import android.view.MotionEvent;
31import android.view.View;
32import android.view.ViewGroup;
33import android.view.Window;
34import android.view.WindowManager;
35import android.view.accessibility.AccessibilityManager;
36import android.widget.SeekBar.OnSeekBarChangeListener;
37
38import com.android.internal.policy.PhoneWindow;
39
40import java.util.Formatter;
41import java.util.Locale;
42
43/**
44 * A view containing controls for a MediaPlayer. Typically contains the
45 * buttons like "Play/Pause", "Rewind", "Fast Forward" and a progress
46 * slider. It takes care of synchronizing the controls with the state
47 * of the MediaPlayer.
48 * <p>
49 * The way to use this class is to instantiate it programmatically.
50 * The MediaController will create a default set of controls
51 * and put them in a window floating above your application. Specifically,
52 * the controls will float above the view specified with setAnchorView().
53 * The window will disappear if left idle for three seconds and reappear
54 * when the user touches the anchor view.
55 * <p>
56 * Functions like show() and hide() have no effect when MediaController
57 * is created in an xml layout.
58 *
59 * MediaController will hide and
60 * show the buttons according to these rules:
61 * <ul>
62 * <li> The "previous" and "next" buttons are hidden until setPrevNextListeners()
63 *   has been called
64 * <li> The "previous" and "next" buttons are visible but disabled if
65 *   setPrevNextListeners() was called with null listeners
66 * <li> The "rewind" and "fastforward" buttons are shown unless requested
67 *   otherwise by using the MediaController(Context, boolean) constructor
68 *   with the boolean set to false
69 * </ul>
70 */
71public class MediaController extends FrameLayout {
72
73    private MediaPlayerControl mPlayer;
74    private final Context mContext;
75    private View mAnchor;
76    private View mRoot;
77    private WindowManager mWindowManager;
78    private Window mWindow;
79    private View mDecor;
80    private WindowManager.LayoutParams mDecorLayoutParams;
81    private ProgressBar mProgress;
82    private TextView mEndTime, mCurrentTime;
83    private boolean mShowing;
84    private boolean mDragging;
85    private static final int sDefaultTimeout = 3000;
86    private static final int FADE_OUT = 1;
87    private static final int SHOW_PROGRESS = 2;
88    private final boolean mUseFastForward;
89    private boolean mFromXml;
90    private boolean mListenersSet;
91    private View.OnClickListener mNextListener, mPrevListener;
92    StringBuilder mFormatBuilder;
93    Formatter mFormatter;
94    private ImageButton mPauseButton;
95    private ImageButton mFfwdButton;
96    private ImageButton mRewButton;
97    private ImageButton mNextButton;
98    private ImageButton mPrevButton;
99    private CharSequence mPlayDescription;
100    private CharSequence mPauseDescription;
101    private final AccessibilityManager mAccessibilityManager;
102
103    public MediaController(Context context, AttributeSet attrs) {
104        super(context, attrs);
105        mRoot = this;
106        mContext = context;
107        mUseFastForward = true;
108        mFromXml = true;
109        mAccessibilityManager = AccessibilityManager.getInstance(context);
110    }
111
112    @Override
113    public void onFinishInflate() {
114        if (mRoot != null)
115            initControllerView(mRoot);
116    }
117
118    public MediaController(Context context, boolean useFastForward) {
119        super(context);
120        mContext = context;
121        mUseFastForward = useFastForward;
122        initFloatingWindowLayout();
123        initFloatingWindow();
124        mAccessibilityManager = AccessibilityManager.getInstance(context);
125    }
126
127    public MediaController(Context context) {
128        this(context, true);
129    }
130
131    private void initFloatingWindow() {
132        mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
133        mWindow = new PhoneWindow(mContext);
134        mWindow.setWindowManager(mWindowManager, null, null);
135        mWindow.requestFeature(Window.FEATURE_NO_TITLE);
136        mDecor = mWindow.getDecorView();
137        mDecor.setOnTouchListener(mTouchListener);
138        mWindow.setContentView(this);
139        mWindow.setBackgroundDrawableResource(android.R.color.transparent);
140
141        // While the media controller is up, the volume control keys should
142        // affect the media stream type
143        mWindow.setVolumeControlStream(AudioManager.STREAM_MUSIC);
144
145        setFocusable(true);
146        setFocusableInTouchMode(true);
147        setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
148        requestFocus();
149    }
150
151    // Allocate and initialize the static parts of mDecorLayoutParams. Must
152    // also call updateFloatingWindowLayout() to fill in the dynamic parts
153    // (y and width) before mDecorLayoutParams can be used.
154    private void initFloatingWindowLayout() {
155        mDecorLayoutParams = new WindowManager.LayoutParams();
156        WindowManager.LayoutParams p = mDecorLayoutParams;
157        p.gravity = Gravity.TOP | Gravity.LEFT;
158        p.height = LayoutParams.WRAP_CONTENT;
159        p.x = 0;
160        p.format = PixelFormat.TRANSLUCENT;
161        p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
162        p.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
163                | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
164                | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
165        p.token = null;
166        p.windowAnimations = 0; // android.R.style.DropDownAnimationDown;
167    }
168
169    // Update the dynamic parts of mDecorLayoutParams
170    // Must be called with mAnchor != NULL.
171    private void updateFloatingWindowLayout() {
172        int [] anchorPos = new int[2];
173        mAnchor.getLocationOnScreen(anchorPos);
174
175        // we need to know the size of the controller so we can properly position it
176        // within its space
177        mDecor.measure(MeasureSpec.makeMeasureSpec(mAnchor.getWidth(), MeasureSpec.AT_MOST),
178                MeasureSpec.makeMeasureSpec(mAnchor.getHeight(), MeasureSpec.AT_MOST));
179
180        WindowManager.LayoutParams p = mDecorLayoutParams;
181        p.width = mAnchor.getWidth();
182        p.x = anchorPos[0] + (mAnchor.getWidth() - p.width) / 2;
183        p.y = anchorPos[1] + mAnchor.getHeight() - mDecor.getMeasuredHeight();
184    }
185
186    // This is called whenever mAnchor's layout bound changes
187    private final OnLayoutChangeListener mLayoutChangeListener =
188            new OnLayoutChangeListener() {
189        @Override
190        public void onLayoutChange(View v, int left, int top, int right,
191                int bottom, int oldLeft, int oldTop, int oldRight,
192                int oldBottom) {
193            updateFloatingWindowLayout();
194            if (mShowing) {
195                mWindowManager.updateViewLayout(mDecor, mDecorLayoutParams);
196            }
197        }
198    };
199
200    private final OnTouchListener mTouchListener = new OnTouchListener() {
201        @Override
202        public boolean onTouch(View v, MotionEvent event) {
203            if (event.getAction() == MotionEvent.ACTION_DOWN) {
204                if (mShowing) {
205                    hide();
206                }
207            }
208            return false;
209        }
210    };
211
212    public void setMediaPlayer(MediaPlayerControl player) {
213        mPlayer = player;
214        updatePausePlay();
215    }
216
217    /**
218     * Set the view that acts as the anchor for the control view.
219     * This can for example be a VideoView, or your Activity's main view.
220     * When VideoView calls this method, it will use the VideoView's parent
221     * as the anchor.
222     * @param view The view to which to anchor the controller when it is visible.
223     */
224    public void setAnchorView(View view) {
225        if (mAnchor != null) {
226            mAnchor.removeOnLayoutChangeListener(mLayoutChangeListener);
227        }
228        mAnchor = view;
229        if (mAnchor != null) {
230            mAnchor.addOnLayoutChangeListener(mLayoutChangeListener);
231        }
232
233        FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams(
234                ViewGroup.LayoutParams.MATCH_PARENT,
235                ViewGroup.LayoutParams.MATCH_PARENT
236        );
237
238        removeAllViews();
239        View v = makeControllerView();
240        addView(v, frameParams);
241    }
242
243    /**
244     * Create the view that holds the widgets that control playback.
245     * Derived classes can override this to create their own.
246     * @return The controller view.
247     * @hide This doesn't work as advertised
248     */
249    protected View makeControllerView() {
250        LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
251        mRoot = inflate.inflate(com.android.internal.R.layout.media_controller, null);
252
253        initControllerView(mRoot);
254
255        return mRoot;
256    }
257
258    private void initControllerView(View v) {
259        Resources res = mContext.getResources();
260        mPlayDescription = res
261                .getText(com.android.internal.R.string.lockscreen_transport_play_description);
262        mPauseDescription = res
263                .getText(com.android.internal.R.string.lockscreen_transport_pause_description);
264        mPauseButton = (ImageButton) v.findViewById(com.android.internal.R.id.pause);
265        if (mPauseButton != null) {
266            mPauseButton.requestFocus();
267            mPauseButton.setOnClickListener(mPauseListener);
268        }
269
270        mFfwdButton = (ImageButton) v.findViewById(com.android.internal.R.id.ffwd);
271        if (mFfwdButton != null) {
272            mFfwdButton.setOnClickListener(mFfwdListener);
273            if (!mFromXml) {
274                mFfwdButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE);
275            }
276        }
277
278        mRewButton = (ImageButton) v.findViewById(com.android.internal.R.id.rew);
279        if (mRewButton != null) {
280            mRewButton.setOnClickListener(mRewListener);
281            if (!mFromXml) {
282                mRewButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE);
283            }
284        }
285
286        // By default these are hidden. They will be enabled when setPrevNextListeners() is called
287        mNextButton = (ImageButton) v.findViewById(com.android.internal.R.id.next);
288        if (mNextButton != null && !mFromXml && !mListenersSet) {
289            mNextButton.setVisibility(View.GONE);
290        }
291        mPrevButton = (ImageButton) v.findViewById(com.android.internal.R.id.prev);
292        if (mPrevButton != null && !mFromXml && !mListenersSet) {
293            mPrevButton.setVisibility(View.GONE);
294        }
295
296        mProgress = (ProgressBar) v.findViewById(com.android.internal.R.id.mediacontroller_progress);
297        if (mProgress != null) {
298            if (mProgress instanceof SeekBar) {
299                SeekBar seeker = (SeekBar) mProgress;
300                seeker.setOnSeekBarChangeListener(mSeekListener);
301            }
302            mProgress.setMax(1000);
303        }
304
305        mEndTime = (TextView) v.findViewById(com.android.internal.R.id.time);
306        mCurrentTime = (TextView) v.findViewById(com.android.internal.R.id.time_current);
307        mFormatBuilder = new StringBuilder();
308        mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());
309
310        installPrevNextListeners();
311    }
312
313    /**
314     * Show the controller on screen. It will go away
315     * automatically after 3 seconds of inactivity.
316     */
317    public void show() {
318        show(sDefaultTimeout);
319    }
320
321    /**
322     * Disable pause or seek buttons if the stream cannot be paused or seeked.
323     * This requires the control interface to be a MediaPlayerControlExt
324     */
325    private void disableUnsupportedButtons() {
326        try {
327            if (mPauseButton != null && !mPlayer.canPause()) {
328                mPauseButton.setEnabled(false);
329            }
330            if (mRewButton != null && !mPlayer.canSeekBackward()) {
331                mRewButton.setEnabled(false);
332            }
333            if (mFfwdButton != null && !mPlayer.canSeekForward()) {
334                mFfwdButton.setEnabled(false);
335            }
336            // TODO What we really should do is add a canSeek to the MediaPlayerControl interface;
337            // this scheme can break the case when applications want to allow seek through the
338            // progress bar but disable forward/backward buttons.
339            //
340            // However, currently the flags SEEK_BACKWARD_AVAILABLE, SEEK_FORWARD_AVAILABLE,
341            // and SEEK_AVAILABLE are all (un)set together; as such the aforementioned issue
342            // shouldn't arise in existing applications.
343            if (mProgress != null && !mPlayer.canSeekBackward() && !mPlayer.canSeekForward()) {
344                mProgress.setEnabled(false);
345            }
346        } catch (IncompatibleClassChangeError ex) {
347            // We were given an old version of the interface, that doesn't have
348            // the canPause/canSeekXYZ methods. This is OK, it just means we
349            // assume the media can be paused and seeked, and so we don't disable
350            // the buttons.
351        }
352    }
353
354    /**
355     * Show the controller on screen. It will go away
356     * automatically after 'timeout' milliseconds of inactivity.
357     * @param timeout The timeout in milliseconds. Use 0 to show
358     * the controller until hide() is called.
359     */
360    public void show(int timeout) {
361        if (!mShowing && mAnchor != null) {
362            setProgress();
363            if (mPauseButton != null) {
364                mPauseButton.requestFocus();
365            }
366            disableUnsupportedButtons();
367            updateFloatingWindowLayout();
368            mWindowManager.addView(mDecor, mDecorLayoutParams);
369            mShowing = true;
370        }
371        updatePausePlay();
372
373        // cause the progress bar to be updated even if mShowing
374        // was already true.  This happens, for example, if we're
375        // paused with the progress bar showing the user hits play.
376        mHandler.sendEmptyMessage(SHOW_PROGRESS);
377
378        if (timeout != 0 && !mAccessibilityManager.isTouchExplorationEnabled()) {
379            mHandler.removeMessages(FADE_OUT);
380            Message msg = mHandler.obtainMessage(FADE_OUT);
381            mHandler.sendMessageDelayed(msg, timeout);
382        }
383    }
384
385    public boolean isShowing() {
386        return mShowing;
387    }
388
389    /**
390     * Remove the controller from the screen.
391     */
392    public void hide() {
393        if (mAnchor == null)
394            return;
395
396        if (mShowing) {
397            try {
398                mHandler.removeMessages(SHOW_PROGRESS);
399                mWindowManager.removeView(mDecor);
400            } catch (IllegalArgumentException ex) {
401                Log.w("MediaController", "already removed");
402            }
403            mShowing = false;
404        }
405    }
406
407    private final Handler mHandler = new Handler() {
408        @Override
409        public void handleMessage(Message msg) {
410            int pos;
411            switch (msg.what) {
412                case FADE_OUT:
413                    hide();
414                    break;
415                case SHOW_PROGRESS:
416                    pos = setProgress();
417                    if (!mDragging && mShowing && mPlayer.isPlaying()) {
418                        msg = obtainMessage(SHOW_PROGRESS);
419                        sendMessageDelayed(msg, 1000 - (pos % 1000));
420                    }
421                    break;
422            }
423        }
424    };
425
426    private String stringForTime(int timeMs) {
427        int totalSeconds = timeMs / 1000;
428
429        int seconds = totalSeconds % 60;
430        int minutes = (totalSeconds / 60) % 60;
431        int hours   = totalSeconds / 3600;
432
433        mFormatBuilder.setLength(0);
434        if (hours > 0) {
435            return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString();
436        } else {
437            return mFormatter.format("%02d:%02d", minutes, seconds).toString();
438        }
439    }
440
441    private int setProgress() {
442        if (mPlayer == null || mDragging) {
443            return 0;
444        }
445        int position = mPlayer.getCurrentPosition();
446        int duration = mPlayer.getDuration();
447        if (mProgress != null) {
448            if (duration > 0) {
449                // use long to avoid overflow
450                long pos = 1000L * position / duration;
451                mProgress.setProgress( (int) pos);
452            }
453            int percent = mPlayer.getBufferPercentage();
454            mProgress.setSecondaryProgress(percent * 10);
455        }
456
457        if (mEndTime != null)
458            mEndTime.setText(stringForTime(duration));
459        if (mCurrentTime != null)
460            mCurrentTime.setText(stringForTime(position));
461
462        return position;
463    }
464
465    @Override
466    public boolean onTouchEvent(MotionEvent event) {
467        switch (event.getAction()) {
468            case MotionEvent.ACTION_DOWN:
469                show(0); // show until hide is called
470                break;
471            case MotionEvent.ACTION_UP:
472                show(sDefaultTimeout); // start timeout
473                break;
474            case MotionEvent.ACTION_CANCEL:
475                hide();
476                break;
477            default:
478                break;
479        }
480        return true;
481    }
482
483    @Override
484    public boolean onTrackballEvent(MotionEvent ev) {
485        show(sDefaultTimeout);
486        return false;
487    }
488
489    @Override
490    public boolean dispatchKeyEvent(KeyEvent event) {
491        int keyCode = event.getKeyCode();
492        final boolean uniqueDown = event.getRepeatCount() == 0
493                && event.getAction() == KeyEvent.ACTION_DOWN;
494        if (keyCode ==  KeyEvent.KEYCODE_HEADSETHOOK
495                || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
496                || keyCode == KeyEvent.KEYCODE_SPACE) {
497            if (uniqueDown) {
498                doPauseResume();
499                show(sDefaultTimeout);
500                if (mPauseButton != null) {
501                    mPauseButton.requestFocus();
502                }
503            }
504            return true;
505        } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
506            if (uniqueDown && !mPlayer.isPlaying()) {
507                mPlayer.start();
508                updatePausePlay();
509                show(sDefaultTimeout);
510            }
511            return true;
512        } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
513                || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
514            if (uniqueDown && mPlayer.isPlaying()) {
515                mPlayer.pause();
516                updatePausePlay();
517                show(sDefaultTimeout);
518            }
519            return true;
520        } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
521                || keyCode == KeyEvent.KEYCODE_VOLUME_UP
522                || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE
523                || keyCode == KeyEvent.KEYCODE_CAMERA) {
524            // don't show the controls for volume adjustment
525            return super.dispatchKeyEvent(event);
526        } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) {
527            if (uniqueDown) {
528                hide();
529            }
530            return true;
531        }
532
533        show(sDefaultTimeout);
534        return super.dispatchKeyEvent(event);
535    }
536
537    private final View.OnClickListener mPauseListener = new View.OnClickListener() {
538        @Override
539        public void onClick(View v) {
540            doPauseResume();
541            show(sDefaultTimeout);
542        }
543    };
544
545    private void updatePausePlay() {
546        if (mRoot == null || mPauseButton == null)
547            return;
548
549        if (mPlayer.isPlaying()) {
550            mPauseButton.setImageResource(com.android.internal.R.drawable.ic_media_pause);
551            mPauseButton.setContentDescription(mPauseDescription);
552        } else {
553            mPauseButton.setImageResource(com.android.internal.R.drawable.ic_media_play);
554            mPauseButton.setContentDescription(mPlayDescription);
555        }
556    }
557
558    private void doPauseResume() {
559        if (mPlayer.isPlaying()) {
560            mPlayer.pause();
561        } else {
562            mPlayer.start();
563        }
564        updatePausePlay();
565    }
566
567    // There are two scenarios that can trigger the seekbar listener to trigger:
568    //
569    // The first is the user using the touchpad to adjust the posititon of the
570    // seekbar's thumb. In this case onStartTrackingTouch is called followed by
571    // a number of onProgressChanged notifications, concluded by onStopTrackingTouch.
572    // We're setting the field "mDragging" to true for the duration of the dragging
573    // session to avoid jumps in the position in case of ongoing playback.
574    //
575    // The second scenario involves the user operating the scroll ball, in this
576    // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications,
577    // we will simply apply the updated position without suspending regular updates.
578    private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
579        @Override
580        public void onStartTrackingTouch(SeekBar bar) {
581            show(3600000);
582
583            mDragging = true;
584
585            // By removing these pending progress messages we make sure
586            // that a) we won't update the progress while the user adjusts
587            // the seekbar and b) once the user is done dragging the thumb
588            // we will post one of these messages to the queue again and
589            // this ensures that there will be exactly one message queued up.
590            mHandler.removeMessages(SHOW_PROGRESS);
591        }
592
593        @Override
594        public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) {
595            if (!fromuser) {
596                // We're not interested in programmatically generated changes to
597                // the progress bar's position.
598                return;
599            }
600
601            long duration = mPlayer.getDuration();
602            long newposition = (duration * progress) / 1000L;
603            mPlayer.seekTo( (int) newposition);
604            if (mCurrentTime != null)
605                mCurrentTime.setText(stringForTime( (int) newposition));
606        }
607
608        @Override
609        public void onStopTrackingTouch(SeekBar bar) {
610            mDragging = false;
611            setProgress();
612            updatePausePlay();
613            show(sDefaultTimeout);
614
615            // Ensure that progress is properly updated in the future,
616            // the call to show() does not guarantee this because it is a
617            // no-op if we are already showing.
618            mHandler.sendEmptyMessage(SHOW_PROGRESS);
619        }
620    };
621
622    @Override
623    public void setEnabled(boolean enabled) {
624        if (mPauseButton != null) {
625            mPauseButton.setEnabled(enabled);
626        }
627        if (mFfwdButton != null) {
628            mFfwdButton.setEnabled(enabled);
629        }
630        if (mRewButton != null) {
631            mRewButton.setEnabled(enabled);
632        }
633        if (mNextButton != null) {
634            mNextButton.setEnabled(enabled && mNextListener != null);
635        }
636        if (mPrevButton != null) {
637            mPrevButton.setEnabled(enabled && mPrevListener != null);
638        }
639        if (mProgress != null) {
640            mProgress.setEnabled(enabled);
641        }
642        disableUnsupportedButtons();
643        super.setEnabled(enabled);
644    }
645
646    @Override
647    public CharSequence getAccessibilityClassName() {
648        return MediaController.class.getName();
649    }
650
651    private final View.OnClickListener mRewListener = new View.OnClickListener() {
652        @Override
653        public void onClick(View v) {
654            int pos = mPlayer.getCurrentPosition();
655            pos -= 5000; // milliseconds
656            mPlayer.seekTo(pos);
657            setProgress();
658
659            show(sDefaultTimeout);
660        }
661    };
662
663    private final View.OnClickListener mFfwdListener = new View.OnClickListener() {
664        @Override
665        public void onClick(View v) {
666            int pos = mPlayer.getCurrentPosition();
667            pos += 15000; // milliseconds
668            mPlayer.seekTo(pos);
669            setProgress();
670
671            show(sDefaultTimeout);
672        }
673    };
674
675    private void installPrevNextListeners() {
676        if (mNextButton != null) {
677            mNextButton.setOnClickListener(mNextListener);
678            mNextButton.setEnabled(mNextListener != null);
679        }
680
681        if (mPrevButton != null) {
682            mPrevButton.setOnClickListener(mPrevListener);
683            mPrevButton.setEnabled(mPrevListener != null);
684        }
685    }
686
687    public void setPrevNextListeners(View.OnClickListener next, View.OnClickListener prev) {
688        mNextListener = next;
689        mPrevListener = prev;
690        mListenersSet = true;
691
692        if (mRoot != null) {
693            installPrevNextListeners();
694
695            if (mNextButton != null && !mFromXml) {
696                mNextButton.setVisibility(View.VISIBLE);
697            }
698            if (mPrevButton != null && !mFromXml) {
699                mPrevButton.setVisibility(View.VISIBLE);
700            }
701        }
702    }
703
704    public interface MediaPlayerControl {
705        void    start();
706        void    pause();
707        int     getDuration();
708        int     getCurrentPosition();
709        void    seekTo(int pos);
710        boolean isPlaying();
711        int     getBufferPercentage();
712        boolean canPause();
713        boolean canSeekBackward();
714        boolean canSeekForward();
715
716        /**
717         * Get the audio session id for the player used by this VideoView. This can be used to
718         * apply audio effects to the audio track of a video.
719         * @return The audio session, or 0 if there was an error.
720         */
721        int     getAudioSessionId();
722    }
723}
724