blob: 65925b446bbef9cf9fa055714a9266018e952a02 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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
Mathew Inwood978c6e22018-08-21 15:58:55 +010019import android.annotation.UnsupportedAppUsage;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080020import android.content.Context;
RoboErik94ca5362014-08-05 18:16:43 -070021import android.content.res.Resources;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080022import android.graphics.PixelFormat;
23import android.media.AudioManager;
Mathew Inwood31755f92018-12-20 13:53:36 +000024import android.os.Build;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080025import 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;
Jae Seo63d37d92015-06-02 16:06:52 -070035import android.view.accessibility.AccessibilityManager;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080036import android.widget.SeekBar.OnSeekBarChangeListener;
37
Jae Seo63d37d92015-06-02 16:06:52 -070038import com.android.internal.policy.PhoneWindow;
39
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080040import 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>
Jae Seo63d37d92015-06-02 16:06:52 -070049 * The way to use this class is to instantiate it programmatically.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080050 * 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.
RoboErik94ca5362014-08-05 18:16:43 -070058 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080059 * 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
Mathew Inwood978c6e22018-08-21 15:58:55 +010073 @UnsupportedAppUsage
RoboErik94ca5362014-08-05 18:16:43 -070074 private MediaPlayerControl mPlayer;
Mathew Inwood978c6e22018-08-21 15:58:55 +010075 @UnsupportedAppUsage
Jae Seo63d37d92015-06-02 16:06:52 -070076 private final Context mContext;
Mathew Inwood978c6e22018-08-21 15:58:55 +010077 @UnsupportedAppUsage
RoboErik94ca5362014-08-05 18:16:43 -070078 private View mAnchor;
Mathew Inwood978c6e22018-08-21 15:58:55 +010079 @UnsupportedAppUsage
RoboErik94ca5362014-08-05 18:16:43 -070080 private View mRoot;
Mathew Inwood978c6e22018-08-21 15:58:55 +010081 @UnsupportedAppUsage
RoboErik94ca5362014-08-05 18:16:43 -070082 private WindowManager mWindowManager;
Mathew Inwood978c6e22018-08-21 15:58:55 +010083 @UnsupportedAppUsage
RoboErik94ca5362014-08-05 18:16:43 -070084 private Window mWindow;
Mathew Inwood978c6e22018-08-21 15:58:55 +010085 @UnsupportedAppUsage
RoboErik94ca5362014-08-05 18:16:43 -070086 private View mDecor;
Mathew Inwood978c6e22018-08-21 15:58:55 +010087 @UnsupportedAppUsage
Chih-Chung Chang85d4ea62011-09-28 17:57:10 +080088 private WindowManager.LayoutParams mDecorLayoutParams;
Mathew Inwood978c6e22018-08-21 15:58:55 +010089 @UnsupportedAppUsage
RoboErik94ca5362014-08-05 18:16:43 -070090 private ProgressBar mProgress;
Mathew Inwood31755f92018-12-20 13:53:36 +000091 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
Mathew Inwood986d7902018-08-20 14:55:21 +010092 private TextView mEndTime;
Mathew Inwood31755f92018-12-20 13:53:36 +000093 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
Mathew Inwood986d7902018-08-20 14:55:21 +010094 private TextView mCurrentTime;
Mathew Inwood978c6e22018-08-21 15:58:55 +010095 @UnsupportedAppUsage
RoboErik94ca5362014-08-05 18:16:43 -070096 private boolean mShowing;
97 private boolean mDragging;
98 private static final int sDefaultTimeout = 3000;
Jae Seo63d37d92015-06-02 16:06:52 -070099 private final boolean mUseFastForward;
RoboErik94ca5362014-08-05 18:16:43 -0700100 private boolean mFromXml;
101 private boolean mListenersSet;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800102 private View.OnClickListener mNextListener, mPrevListener;
RoboErik94ca5362014-08-05 18:16:43 -0700103 StringBuilder mFormatBuilder;
104 Formatter mFormatter;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100105 @UnsupportedAppUsage
RoboErik94ca5362014-08-05 18:16:43 -0700106 private ImageButton mPauseButton;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100107 @UnsupportedAppUsage
RoboErik94ca5362014-08-05 18:16:43 -0700108 private ImageButton mFfwdButton;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100109 @UnsupportedAppUsage
RoboErik94ca5362014-08-05 18:16:43 -0700110 private ImageButton mRewButton;
Mathew Inwood31755f92018-12-20 13:53:36 +0000111 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
RoboErik94ca5362014-08-05 18:16:43 -0700112 private ImageButton mNextButton;
Mathew Inwood31755f92018-12-20 13:53:36 +0000113 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
RoboErik94ca5362014-08-05 18:16:43 -0700114 private ImageButton mPrevButton;
115 private CharSequence mPlayDescription;
116 private CharSequence mPauseDescription;
Jae Seo63d37d92015-06-02 16:06:52 -0700117 private final AccessibilityManager mAccessibilityManager;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800118
119 public MediaController(Context context, AttributeSet attrs) {
120 super(context, attrs);
121 mRoot = this;
122 mContext = context;
123 mUseFastForward = true;
124 mFromXml = true;
Jae Seo63d37d92015-06-02 16:06:52 -0700125 mAccessibilityManager = AccessibilityManager.getInstance(context);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800126 }
127
128 @Override
129 public void onFinishInflate() {
130 if (mRoot != null)
131 initControllerView(mRoot);
132 }
133
134 public MediaController(Context context, boolean useFastForward) {
135 super(context);
136 mContext = context;
137 mUseFastForward = useFastForward;
Chih-Chung Chang02dd17d2011-09-29 12:49:04 +0800138 initFloatingWindowLayout();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800139 initFloatingWindow();
Jae Seo63d37d92015-06-02 16:06:52 -0700140 mAccessibilityManager = AccessibilityManager.getInstance(context);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800141 }
142
143 public MediaController(Context context) {
Chih-Chung Chang02dd17d2011-09-29 12:49:04 +0800144 this(context, true);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800145 }
146
147 private void initFloatingWindow() {
Christian Mehlmaueref367522010-05-31 23:08:30 +0200148 mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
Jorim Jaggib10e33f2015-02-04 21:57:40 +0100149 mWindow = new PhoneWindow(mContext);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800150 mWindow.setWindowManager(mWindowManager, null, null);
151 mWindow.requestFeature(Window.FEATURE_NO_TITLE);
152 mDecor = mWindow.getDecorView();
153 mDecor.setOnTouchListener(mTouchListener);
154 mWindow.setContentView(this);
155 mWindow.setBackgroundDrawableResource(android.R.color.transparent);
RoboErik94ca5362014-08-05 18:16:43 -0700156
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800157 // While the media controller is up, the volume control keys should
158 // affect the media stream type
159 mWindow.setVolumeControlStream(AudioManager.STREAM_MUSIC);
160
161 setFocusable(true);
162 setFocusableInTouchMode(true);
163 setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
164 requestFocus();
165 }
166
Chih-Chung Chang85d4ea62011-09-28 17:57:10 +0800167 // Allocate and initialize the static parts of mDecorLayoutParams. Must
168 // also call updateFloatingWindowLayout() to fill in the dynamic parts
169 // (y and width) before mDecorLayoutParams can be used.
170 private void initFloatingWindowLayout() {
171 mDecorLayoutParams = new WindowManager.LayoutParams();
172 WindowManager.LayoutParams p = mDecorLayoutParams;
Marco Nelissen0b2d8722012-11-12 15:20:57 -0800173 p.gravity = Gravity.TOP | Gravity.LEFT;
Chih-Chung Chang85d4ea62011-09-28 17:57:10 +0800174 p.height = LayoutParams.WRAP_CONTENT;
175 p.x = 0;
176 p.format = PixelFormat.TRANSLUCENT;
177 p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
178 p.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
179 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
180 | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
181 p.token = null;
182 p.windowAnimations = 0; // android.R.style.DropDownAnimationDown;
183 }
184
185 // Update the dynamic parts of mDecorLayoutParams
186 // Must be called with mAnchor != NULL.
187 private void updateFloatingWindowLayout() {
188 int [] anchorPos = new int[2];
189 mAnchor.getLocationOnScreen(anchorPos);
190
Marco Nelissen0b2d8722012-11-12 15:20:57 -0800191 // we need to know the size of the controller so we can properly position it
192 // within its space
193 mDecor.measure(MeasureSpec.makeMeasureSpec(mAnchor.getWidth(), MeasureSpec.AT_MOST),
194 MeasureSpec.makeMeasureSpec(mAnchor.getHeight(), MeasureSpec.AT_MOST));
195
Chih-Chung Chang85d4ea62011-09-28 17:57:10 +0800196 WindowManager.LayoutParams p = mDecorLayoutParams;
197 p.width = mAnchor.getWidth();
Marco Nelissen0b2d8722012-11-12 15:20:57 -0800198 p.x = anchorPos[0] + (mAnchor.getWidth() - p.width) / 2;
199 p.y = anchorPos[1] + mAnchor.getHeight() - mDecor.getMeasuredHeight();
Chih-Chung Chang85d4ea62011-09-28 17:57:10 +0800200 }
201
202 // This is called whenever mAnchor's layout bound changes
Jae Seo63d37d92015-06-02 16:06:52 -0700203 private final OnLayoutChangeListener mLayoutChangeListener =
Chih-Chung Chang85d4ea62011-09-28 17:57:10 +0800204 new OnLayoutChangeListener() {
Jae Seo63d37d92015-06-02 16:06:52 -0700205 @Override
Chih-Chung Chang85d4ea62011-09-28 17:57:10 +0800206 public void onLayoutChange(View v, int left, int top, int right,
207 int bottom, int oldLeft, int oldTop, int oldRight,
208 int oldBottom) {
209 updateFloatingWindowLayout();
210 if (mShowing) {
211 mWindowManager.updateViewLayout(mDecor, mDecorLayoutParams);
212 }
213 }
214 };
215
Jae Seo63d37d92015-06-02 16:06:52 -0700216 private final OnTouchListener mTouchListener = new OnTouchListener() {
217 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800218 public boolean onTouch(View v, MotionEvent event) {
219 if (event.getAction() == MotionEvent.ACTION_DOWN) {
220 if (mShowing) {
221 hide();
222 }
223 }
224 return false;
225 }
226 };
RoboErik94ca5362014-08-05 18:16:43 -0700227
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800228 public void setMediaPlayer(MediaPlayerControl player) {
229 mPlayer = player;
230 updatePausePlay();
231 }
232
233 /**
234 * Set the view that acts as the anchor for the control view.
235 * This can for example be a VideoView, or your Activity's main view.
Marco Nelissen0b2d8722012-11-12 15:20:57 -0800236 * When VideoView calls this method, it will use the VideoView's parent
237 * as the anchor.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800238 * @param view The view to which to anchor the controller when it is visible.
239 */
240 public void setAnchorView(View view) {
Chih-Chung Chang85d4ea62011-09-28 17:57:10 +0800241 if (mAnchor != null) {
242 mAnchor.removeOnLayoutChangeListener(mLayoutChangeListener);
243 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800244 mAnchor = view;
Chih-Chung Chang85d4ea62011-09-28 17:57:10 +0800245 if (mAnchor != null) {
246 mAnchor.addOnLayoutChangeListener(mLayoutChangeListener);
247 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800248
249 FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams(
Romain Guy980a9382010-01-08 15:06:28 -0800250 ViewGroup.LayoutParams.MATCH_PARENT,
251 ViewGroup.LayoutParams.MATCH_PARENT
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800252 );
253
254 removeAllViews();
255 View v = makeControllerView();
256 addView(v, frameParams);
257 }
258
259 /**
260 * Create the view that holds the widgets that control playback.
261 * Derived classes can override this to create their own.
262 * @return The controller view.
263 * @hide This doesn't work as advertised
264 */
265 protected View makeControllerView() {
266 LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
267 mRoot = inflate.inflate(com.android.internal.R.layout.media_controller, null);
268
269 initControllerView(mRoot);
270
271 return mRoot;
272 }
273
274 private void initControllerView(View v) {
RoboErik94ca5362014-08-05 18:16:43 -0700275 Resources res = mContext.getResources();
276 mPlayDescription = res
277 .getText(com.android.internal.R.string.lockscreen_transport_play_description);
278 mPauseDescription = res
279 .getText(com.android.internal.R.string.lockscreen_transport_pause_description);
Alan Viverette8e1a7292017-02-27 10:57:58 -0500280 mPauseButton = v.findViewById(com.android.internal.R.id.pause);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800281 if (mPauseButton != null) {
282 mPauseButton.requestFocus();
283 mPauseButton.setOnClickListener(mPauseListener);
284 }
285
Alan Viverette8e1a7292017-02-27 10:57:58 -0500286 mFfwdButton = v.findViewById(com.android.internal.R.id.ffwd);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800287 if (mFfwdButton != null) {
288 mFfwdButton.setOnClickListener(mFfwdListener);
289 if (!mFromXml) {
290 mFfwdButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE);
291 }
292 }
293
Alan Viverette8e1a7292017-02-27 10:57:58 -0500294 mRewButton = v.findViewById(com.android.internal.R.id.rew);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800295 if (mRewButton != null) {
296 mRewButton.setOnClickListener(mRewListener);
297 if (!mFromXml) {
298 mRewButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE);
299 }
300 }
301
RoboErik94ca5362014-08-05 18:16:43 -0700302 // By default these are hidden. They will be enabled when setPrevNextListeners() is called
Alan Viverette8e1a7292017-02-27 10:57:58 -0500303 mNextButton = v.findViewById(com.android.internal.R.id.next);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800304 if (mNextButton != null && !mFromXml && !mListenersSet) {
305 mNextButton.setVisibility(View.GONE);
306 }
Alan Viverette8e1a7292017-02-27 10:57:58 -0500307 mPrevButton = v.findViewById(com.android.internal.R.id.prev);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800308 if (mPrevButton != null && !mFromXml && !mListenersSet) {
309 mPrevButton.setVisibility(View.GONE);
310 }
311
Alan Viverette8e1a7292017-02-27 10:57:58 -0500312 mProgress = v.findViewById(com.android.internal.R.id.mediacontroller_progress);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800313 if (mProgress != null) {
314 if (mProgress instanceof SeekBar) {
315 SeekBar seeker = (SeekBar) mProgress;
316 seeker.setOnSeekBarChangeListener(mSeekListener);
317 }
318 mProgress.setMax(1000);
319 }
320
Alan Viverette8e1a7292017-02-27 10:57:58 -0500321 mEndTime = v.findViewById(com.android.internal.R.id.time);
322 mCurrentTime = v.findViewById(com.android.internal.R.id.time_current);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800323 mFormatBuilder = new StringBuilder();
324 mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());
325
326 installPrevNextListeners();
327 }
328
329 /**
330 * Show the controller on screen. It will go away
331 * automatically after 3 seconds of inactivity.
332 */
333 public void show() {
334 show(sDefaultTimeout);
335 }
336
337 /**
Marco Nelissenc818b142009-08-19 08:32:21 -0700338 * Disable pause or seek buttons if the stream cannot be paused or seeked.
339 * This requires the control interface to be a MediaPlayerControlExt
340 */
341 private void disableUnsupportedButtons() {
342 try {
343 if (mPauseButton != null && !mPlayer.canPause()) {
344 mPauseButton.setEnabled(false);
345 }
346 if (mRewButton != null && !mPlayer.canSeekBackward()) {
347 mRewButton.setEnabled(false);
348 }
349 if (mFfwdButton != null && !mPlayer.canSeekForward()) {
350 mFfwdButton.setEnabled(false);
351 }
Robert Shih39dbbd92015-04-27 12:50:39 -0700352 // TODO What we really should do is add a canSeek to the MediaPlayerControl interface;
353 // this scheme can break the case when applications want to allow seek through the
354 // progress bar but disable forward/backward buttons.
355 //
356 // However, currently the flags SEEK_BACKWARD_AVAILABLE, SEEK_FORWARD_AVAILABLE,
357 // and SEEK_AVAILABLE are all (un)set together; as such the aforementioned issue
358 // shouldn't arise in existing applications.
359 if (mProgress != null && !mPlayer.canSeekBackward() && !mPlayer.canSeekForward()) {
360 mProgress.setEnabled(false);
361 }
Marco Nelissenc818b142009-08-19 08:32:21 -0700362 } catch (IncompatibleClassChangeError ex) {
363 // We were given an old version of the interface, that doesn't have
364 // the canPause/canSeekXYZ methods. This is OK, it just means we
365 // assume the media can be paused and seeked, and so we don't disable
366 // the buttons.
367 }
368 }
RoboErik94ca5362014-08-05 18:16:43 -0700369
Marco Nelissenc818b142009-08-19 08:32:21 -0700370 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800371 * Show the controller on screen. It will go away
372 * automatically after 'timeout' milliseconds of inactivity.
373 * @param timeout The timeout in milliseconds. Use 0 to show
374 * the controller until hide() is called.
375 */
376 public void show(int timeout) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800377 if (!mShowing && mAnchor != null) {
378 setProgress();
Marco Nelissend701e022009-08-19 15:39:23 -0700379 if (mPauseButton != null) {
380 mPauseButton.requestFocus();
381 }
Marco Nelissenc818b142009-08-19 08:32:21 -0700382 disableUnsupportedButtons();
Chih-Chung Chang85d4ea62011-09-28 17:57:10 +0800383 updateFloatingWindowLayout();
384 mWindowManager.addView(mDecor, mDecorLayoutParams);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800385 mShowing = true;
386 }
387 updatePausePlay();
RoboErik94ca5362014-08-05 18:16:43 -0700388
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800389 // cause the progress bar to be updated even if mShowing
390 // was already true. This happens, for example, if we're
391 // paused with the progress bar showing the user hits play.
John Reckd0374c62015-10-20 13:25:01 -0700392 post(mShowProgress);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800393
Jae Seo63d37d92015-06-02 16:06:52 -0700394 if (timeout != 0 && !mAccessibilityManager.isTouchExplorationEnabled()) {
John Reckd0374c62015-10-20 13:25:01 -0700395 removeCallbacks(mFadeOut);
396 postDelayed(mFadeOut, timeout);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800397 }
398 }
RoboErik94ca5362014-08-05 18:16:43 -0700399
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800400 public boolean isShowing() {
401 return mShowing;
402 }
403
404 /**
405 * Remove the controller from the screen.
406 */
407 public void hide() {
408 if (mAnchor == null)
409 return;
410
411 if (mShowing) {
412 try {
John Reckd0374c62015-10-20 13:25:01 -0700413 removeCallbacks(mShowProgress);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800414 mWindowManager.removeView(mDecor);
415 } catch (IllegalArgumentException ex) {
416 Log.w("MediaController", "already removed");
417 }
418 mShowing = false;
419 }
420 }
421
John Reckd0374c62015-10-20 13:25:01 -0700422 private final Runnable mFadeOut = new Runnable() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800423 @Override
John Reckd0374c62015-10-20 13:25:01 -0700424 public void run() {
425 hide();
426 }
427 };
428
429 private final Runnable mShowProgress = new Runnable() {
430 @Override
431 public void run() {
432 int pos = setProgress();
433 if (!mDragging && mShowing && mPlayer.isPlaying()) {
434 postDelayed(mShowProgress, 1000 - (pos % 1000));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800435 }
436 }
437 };
438
439 private String stringForTime(int timeMs) {
440 int totalSeconds = timeMs / 1000;
441
442 int seconds = totalSeconds % 60;
443 int minutes = (totalSeconds / 60) % 60;
444 int hours = totalSeconds / 3600;
445
446 mFormatBuilder.setLength(0);
447 if (hours > 0) {
448 return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString();
449 } else {
450 return mFormatter.format("%02d:%02d", minutes, seconds).toString();
451 }
452 }
453
454 private int setProgress() {
455 if (mPlayer == null || mDragging) {
456 return 0;
457 }
458 int position = mPlayer.getCurrentPosition();
459 int duration = mPlayer.getDuration();
460 if (mProgress != null) {
461 if (duration > 0) {
462 // use long to avoid overflow
463 long pos = 1000L * position / duration;
464 mProgress.setProgress( (int) pos);
465 }
466 int percent = mPlayer.getBufferPercentage();
467 mProgress.setSecondaryProgress(percent * 10);
468 }
469
470 if (mEndTime != null)
471 mEndTime.setText(stringForTime(duration));
472 if (mCurrentTime != null)
473 mCurrentTime.setText(stringForTime(position));
474
475 return position;
476 }
477
478 @Override
479 public boolean onTouchEvent(MotionEvent event) {
Robert Shihcfe4b592013-11-06 11:53:56 -0800480 switch (event.getAction()) {
481 case MotionEvent.ACTION_DOWN:
482 show(0); // show until hide is called
483 break;
484 case MotionEvent.ACTION_UP:
485 show(sDefaultTimeout); // start timeout
486 break;
487 case MotionEvent.ACTION_CANCEL:
488 hide();
489 break;
490 default:
491 break;
492 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800493 return true;
494 }
495
496 @Override
497 public boolean onTrackballEvent(MotionEvent ev) {
498 show(sDefaultTimeout);
499 return false;
500 }
501
502 @Override
503 public boolean dispatchKeyEvent(KeyEvent event) {
504 int keyCode = event.getKeyCode();
Jeff Brown4d396052010-10-29 21:50:21 -0700505 final boolean uniqueDown = event.getRepeatCount() == 0
506 && event.getAction() == KeyEvent.ACTION_DOWN;
507 if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK
508 || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
509 || keyCode == KeyEvent.KEYCODE_SPACE) {
510 if (uniqueDown) {
511 doPauseResume();
512 show(sDefaultTimeout);
513 if (mPauseButton != null) {
514 mPauseButton.requestFocus();
515 }
Marco Nelissend701e022009-08-19 15:39:23 -0700516 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800517 return true;
Jeff Brown4d396052010-10-29 21:50:21 -0700518 } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
519 if (uniqueDown && !mPlayer.isPlaying()) {
520 mPlayer.start();
521 updatePausePlay();
522 show(sDefaultTimeout);
523 }
524 return true;
525 } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
526 || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
527 if (uniqueDown && mPlayer.isPlaying()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800528 mPlayer.pause();
529 updatePausePlay();
Jeff Brown4d396052010-10-29 21:50:21 -0700530 show(sDefaultTimeout);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800531 }
532 return true;
Jeff Brown4d396052010-10-29 21:50:21 -0700533 } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
Jeff Brownb0418da2010-11-01 15:24:01 -0700534 || keyCode == KeyEvent.KEYCODE_VOLUME_UP
bxu10X96ac7202012-04-10 16:52:36 +0800535 || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE
536 || keyCode == KeyEvent.KEYCODE_CAMERA) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800537 // don't show the controls for volume adjustment
538 return super.dispatchKeyEvent(event);
539 } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) {
Jeff Brown4d396052010-10-29 21:50:21 -0700540 if (uniqueDown) {
541 hide();
542 }
The Android Open Source Project10592532009-03-18 17:39:46 -0700543 return true;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800544 }
Jeff Brown4d396052010-10-29 21:50:21 -0700545
546 show(sDefaultTimeout);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800547 return super.dispatchKeyEvent(event);
548 }
549
Jae Seo63d37d92015-06-02 16:06:52 -0700550 private final View.OnClickListener mPauseListener = new View.OnClickListener() {
551 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800552 public void onClick(View v) {
553 doPauseResume();
554 show(sDefaultTimeout);
555 }
556 };
557
Mathew Inwood978c6e22018-08-21 15:58:55 +0100558 @UnsupportedAppUsage
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800559 private void updatePausePlay() {
Marco Nelissenc818b142009-08-19 08:32:21 -0700560 if (mRoot == null || mPauseButton == null)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800561 return;
562
563 if (mPlayer.isPlaying()) {
Marco Nelissenc818b142009-08-19 08:32:21 -0700564 mPauseButton.setImageResource(com.android.internal.R.drawable.ic_media_pause);
RoboErik94ca5362014-08-05 18:16:43 -0700565 mPauseButton.setContentDescription(mPauseDescription);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800566 } else {
Marco Nelissenc818b142009-08-19 08:32:21 -0700567 mPauseButton.setImageResource(com.android.internal.R.drawable.ic_media_play);
RoboErik94ca5362014-08-05 18:16:43 -0700568 mPauseButton.setContentDescription(mPlayDescription);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800569 }
570 }
571
572 private void doPauseResume() {
573 if (mPlayer.isPlaying()) {
574 mPlayer.pause();
575 } else {
576 mPlayer.start();
577 }
578 updatePausePlay();
579 }
580
Andreas Huber0de7dcd2009-04-20 14:22:31 -0700581 // There are two scenarios that can trigger the seekbar listener to trigger:
582 //
583 // The first is the user using the touchpad to adjust the posititon of the
584 // seekbar's thumb. In this case onStartTrackingTouch is called followed by
585 // a number of onProgressChanged notifications, concluded by onStopTrackingTouch.
586 // We're setting the field "mDragging" to true for the duration of the dragging
587 // session to avoid jumps in the position in case of ongoing playback.
588 //
589 // The second scenario involves the user operating the scroll ball, in this
590 // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications,
591 // we will simply apply the updated position without suspending regular updates.
Mathew Inwood978c6e22018-08-21 15:58:55 +0100592 @UnsupportedAppUsage
Jae Seo63d37d92015-06-02 16:06:52 -0700593 private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
594 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800595 public void onStartTrackingTouch(SeekBar bar) {
596 show(3600000);
Andreas Huber0de7dcd2009-04-20 14:22:31 -0700597
598 mDragging = true;
599
600 // By removing these pending progress messages we make sure
601 // that a) we won't update the progress while the user adjusts
602 // the seekbar and b) once the user is done dragging the thumb
603 // we will post one of these messages to the queue again and
604 // this ensures that there will be exactly one message queued up.
John Reckd0374c62015-10-20 13:25:01 -0700605 removeCallbacks(mShowProgress);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800606 }
Andreas Huber0de7dcd2009-04-20 14:22:31 -0700607
Jae Seo63d37d92015-06-02 16:06:52 -0700608 @Override
Andreas Huber0de7dcd2009-04-20 14:22:31 -0700609 public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) {
610 if (!fromuser) {
611 // We're not interested in programmatically generated changes to
612 // the progress bar's position.
613 return;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800614 }
Andreas Huber0de7dcd2009-04-20 14:22:31 -0700615
616 long duration = mPlayer.getDuration();
617 long newposition = (duration * progress) / 1000L;
618 mPlayer.seekTo( (int) newposition);
619 if (mCurrentTime != null)
620 mCurrentTime.setText(stringForTime( (int) newposition));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800621 }
Andreas Huber0de7dcd2009-04-20 14:22:31 -0700622
Jae Seo63d37d92015-06-02 16:06:52 -0700623 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800624 public void onStopTrackingTouch(SeekBar bar) {
625 mDragging = false;
626 setProgress();
627 updatePausePlay();
628 show(sDefaultTimeout);
Andreas Huber0de7dcd2009-04-20 14:22:31 -0700629
630 // Ensure that progress is properly updated in the future,
631 // the call to show() does not guarantee this because it is a
632 // no-op if we are already showing.
John Reckd0374c62015-10-20 13:25:01 -0700633 post(mShowProgress);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800634 }
635 };
636
637 @Override
638 public void setEnabled(boolean enabled) {
639 if (mPauseButton != null) {
640 mPauseButton.setEnabled(enabled);
641 }
642 if (mFfwdButton != null) {
643 mFfwdButton.setEnabled(enabled);
644 }
645 if (mRewButton != null) {
646 mRewButton.setEnabled(enabled);
647 }
648 if (mNextButton != null) {
649 mNextButton.setEnabled(enabled && mNextListener != null);
650 }
651 if (mPrevButton != null) {
652 mPrevButton.setEnabled(enabled && mPrevListener != null);
653 }
654 if (mProgress != null) {
655 mProgress.setEnabled(enabled);
656 }
Marco Nelissenc818b142009-08-19 08:32:21 -0700657 disableUnsupportedButtons();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800658 super.setEnabled(enabled);
659 }
660
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800661 @Override
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -0800662 public CharSequence getAccessibilityClassName() {
663 return MediaController.class.getName();
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800664 }
665
Mathew Inwood978c6e22018-08-21 15:58:55 +0100666 @UnsupportedAppUsage
Jae Seo63d37d92015-06-02 16:06:52 -0700667 private final View.OnClickListener mRewListener = new View.OnClickListener() {
668 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800669 public void onClick(View v) {
670 int pos = mPlayer.getCurrentPosition();
671 pos -= 5000; // milliseconds
672 mPlayer.seekTo(pos);
673 setProgress();
674
675 show(sDefaultTimeout);
676 }
677 };
678
Mathew Inwood978c6e22018-08-21 15:58:55 +0100679 @UnsupportedAppUsage
Jae Seo63d37d92015-06-02 16:06:52 -0700680 private final View.OnClickListener mFfwdListener = new View.OnClickListener() {
681 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800682 public void onClick(View v) {
683 int pos = mPlayer.getCurrentPosition();
684 pos += 15000; // milliseconds
685 mPlayer.seekTo(pos);
686 setProgress();
687
688 show(sDefaultTimeout);
689 }
690 };
691
692 private void installPrevNextListeners() {
693 if (mNextButton != null) {
694 mNextButton.setOnClickListener(mNextListener);
695 mNextButton.setEnabled(mNextListener != null);
696 }
697
698 if (mPrevButton != null) {
699 mPrevButton.setOnClickListener(mPrevListener);
700 mPrevButton.setEnabled(mPrevListener != null);
701 }
702 }
703
704 public void setPrevNextListeners(View.OnClickListener next, View.OnClickListener prev) {
705 mNextListener = next;
706 mPrevListener = prev;
707 mListenersSet = true;
708
709 if (mRoot != null) {
710 installPrevNextListeners();
RoboErik94ca5362014-08-05 18:16:43 -0700711
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800712 if (mNextButton != null && !mFromXml) {
713 mNextButton.setVisibility(View.VISIBLE);
714 }
715 if (mPrevButton != null && !mFromXml) {
716 mPrevButton.setVisibility(View.VISIBLE);
717 }
718 }
719 }
720
721 public interface MediaPlayerControl {
722 void start();
723 void pause();
724 int getDuration();
725 int getCurrentPosition();
726 void seekTo(int pos);
727 boolean isPlaying();
728 int getBufferPercentage();
Marco Nelissenc818b142009-08-19 08:32:21 -0700729 boolean canPause();
730 boolean canSeekBackward();
731 boolean canSeekForward();
Marco Nelissen13bfebd2013-05-09 15:36:53 -0700732
733 /**
734 * Get the audio session id for the player used by this VideoView. This can be used to
735 * apply audio effects to the audio track of a video.
736 * @return The audio session, or 0 if there was an error.
737 */
738 int getAudioSessionId();
Marco Nelissenc818b142009-08-19 08:32:21 -0700739 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800740}