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