OLD | NEW |
(Empty) | |
| 1 /* |
| 2 * Copyright (C) 2013 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 |
| 17 package org.chromium.third_party.android.media; |
| 18 |
| 19 import android.content.Context; |
| 20 import android.support.v4.media.session.PlaybackStateCompat; |
| 21 import android.util.AttributeSet; |
| 22 import android.view.LayoutInflater; |
| 23 import android.view.View; |
| 24 import android.view.ViewGroup; |
| 25 import android.view.accessibility.AccessibilityEvent; |
| 26 import android.view.accessibility.AccessibilityNodeInfo; |
| 27 import android.widget.FrameLayout; |
| 28 import android.widget.ImageButton; |
| 29 import android.widget.SeekBar; |
| 30 import android.widget.TextView; |
| 31 |
| 32 import org.chromium.third_party.android.media.R; |
| 33 |
| 34 import java.util.Formatter; |
| 35 import java.util.Locale; |
| 36 |
| 37 /** |
| 38 * Helper for implementing media controls in an application. |
| 39 * We use this custom version instead of {@link android.widget.MediaController}
so that we can |
| 40 * customize the look as we want. This file is taken directly from the public An
droid sample for |
| 41 * supportv4, with tiny bug fixes. |
| 42 */ |
| 43 public class MediaController extends FrameLayout { |
| 44 /** |
| 45 * The interface that allows media controller to actually control media and
provides some |
| 46 * essential metadata for the UI like the current position, duration, etc. |
| 47 */ |
| 48 public interface Delegate { |
| 49 /** |
| 50 * Called when the user wants to resume or start. |
| 51 */ |
| 52 void play(); |
| 53 |
| 54 /** |
| 55 * Called when the user wants to pause. |
| 56 */ |
| 57 void pause(); |
| 58 |
| 59 /** |
| 60 * Called when the user wants to seek. |
| 61 */ |
| 62 void seekTo(long pos); |
| 63 |
| 64 /** |
| 65 * @return the current media duration, in milliseconds. |
| 66 */ |
| 67 long getDuration(); |
| 68 |
| 69 /** |
| 70 * @return the current playback position, in milliseconds. |
| 71 */ |
| 72 long getPosition(); |
| 73 |
| 74 /** |
| 75 * @return if the media is currently playing. |
| 76 */ |
| 77 boolean isPlaying(); |
| 78 |
| 79 /** |
| 80 * @return a combination of {@link PlaybackStateCompat} flags defining w
hat UI elements will |
| 81 * be available to the user. |
| 82 */ |
| 83 long getActionFlags(); |
| 84 } |
| 85 |
| 86 private Delegate mDelegate; |
| 87 private Context mContext; |
| 88 private ViewGroup mProgressGroup; |
| 89 private SeekBar mProgressBar; |
| 90 private TextView mEndTime, mCurrentTime; |
| 91 private boolean mDragging; |
| 92 private boolean mUseFastForward; |
| 93 private boolean mListenersSet; |
| 94 private boolean mShowNext, mShowPrev; |
| 95 private View.OnClickListener mNextListener, mPrevListener; |
| 96 private StringBuilder mFormatBuilder; |
| 97 private Formatter mFormatter; |
| 98 private ImageButton mPauseButton; |
| 99 private ImageButton mFfwdButton; |
| 100 private ImageButton mRewButton; |
| 101 private ImageButton mNextButton; |
| 102 private ImageButton mPrevButton; |
| 103 |
| 104 public MediaController(Context context, AttributeSet attrs) { |
| 105 super(context, attrs); |
| 106 mContext = context; |
| 107 mUseFastForward = true; |
| 108 LayoutInflater inflate = |
| 109 (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLAT
ER_SERVICE); |
| 110 inflate.inflate(R.layout.media_controller, this, true); |
| 111 initControllerView(); |
| 112 } |
| 113 |
| 114 public MediaController(Context context, boolean useFastForward) { |
| 115 super(context); |
| 116 mContext = context; |
| 117 mUseFastForward = useFastForward; |
| 118 } |
| 119 |
| 120 public MediaController(Context context) { |
| 121 this(context, true); |
| 122 } |
| 123 |
| 124 public void setDelegate(Delegate delegate) { |
| 125 mDelegate = delegate; |
| 126 updatePausePlay(); |
| 127 } |
| 128 |
| 129 private void initControllerView() { |
| 130 mPauseButton = (ImageButton) findViewById(R.id.pause); |
| 131 if (mPauseButton != null) { |
| 132 mPauseButton.requestFocus(); |
| 133 mPauseButton.setOnClickListener(mPauseListener); |
| 134 } |
| 135 |
| 136 mFfwdButton = (ImageButton) findViewById(R.id.ffwd); |
| 137 if (mFfwdButton != null) { |
| 138 mFfwdButton.setOnClickListener(mFfwdListener); |
| 139 mFfwdButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE
); |
| 140 } |
| 141 |
| 142 mRewButton = (ImageButton) findViewById(R.id.rew); |
| 143 if (mRewButton != null) { |
| 144 mRewButton.setOnClickListener(mRewListener); |
| 145 mRewButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE)
; |
| 146 } |
| 147 |
| 148 // By default these are hidden. They will be enabled when setPrevNextLis
teners() is called |
| 149 mNextButton = (ImageButton) findViewById(R.id.next); |
| 150 if (mNextButton != null && !mListenersSet) { |
| 151 mNextButton.setVisibility(View.GONE); |
| 152 } |
| 153 mPrevButton = (ImageButton) findViewById(R.id.prev); |
| 154 if (mPrevButton != null && !mListenersSet) { |
| 155 mPrevButton.setVisibility(View.GONE); |
| 156 } |
| 157 |
| 158 mProgressGroup = (ViewGroup) findViewById(R.id.mediacontroller_progress_
container); |
| 159 |
| 160 if (mProgressGroup != null) { |
| 161 mProgressBar = (SeekBar) mProgressGroup.findViewById(R.id.mediacontr
oller_progress_bar); |
| 162 if (mProgressBar != null) { |
| 163 mProgressBar.setOnSeekBarChangeListener(mSeekListener); |
| 164 mProgressBar.setMax(1000); |
| 165 } |
| 166 } |
| 167 |
| 168 mEndTime = (TextView) findViewById(R.id.time); |
| 169 mCurrentTime = (TextView) findViewById(R.id.time_current); |
| 170 mFormatBuilder = new StringBuilder(); |
| 171 mFormatter = new Formatter(mFormatBuilder, Locale.getDefault()); |
| 172 |
| 173 installPrevNextListeners(); |
| 174 } |
| 175 |
| 176 /** |
| 177 * Disable pause or seek buttons if the stream cannot be paused or seeked. |
| 178 * This requires the control interface to be a MediaPlayerControlExt |
| 179 */ |
| 180 void updateButtons() { |
| 181 if (mDelegate == null) return; |
| 182 |
| 183 long flags = mDelegate.getActionFlags(); |
| 184 boolean enabled = isEnabled(); |
| 185 if (mPauseButton != null) { |
| 186 boolean needPlayPauseButton = (flags & PlaybackStateCompat.ACTION_PL
AY) != 0 |
| 187 || (flags & PlaybackStateCompat.ACTION_PAUSE) != 0; |
| 188 mPauseButton.setEnabled(enabled && needPlayPauseButton); |
| 189 } |
| 190 if (mRewButton != null) { |
| 191 mRewButton.setEnabled(enabled && (flags & PlaybackStateCompat.ACTION
_REWIND) != 0); |
| 192 } |
| 193 if (mFfwdButton != null) { |
| 194 mFfwdButton.setEnabled( |
| 195 enabled && (flags & PlaybackStateCompat.ACTION_FAST_FORWARD)
!= 0); |
| 196 } |
| 197 if (mPrevButton != null) { |
| 198 mShowPrev = |
| 199 (flags & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0 || mP
revListener != null; |
| 200 mPrevButton.setEnabled(enabled && mShowPrev); |
| 201 } |
| 202 if (mNextButton != null) { |
| 203 mShowNext = (flags & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) !=
0 |
| 204 || mNextListener != null; |
| 205 mNextButton.setEnabled(enabled && mShowNext); |
| 206 } |
| 207 } |
| 208 |
| 209 public void refresh() { |
| 210 updateProgress(); |
| 211 updateButtons(); |
| 212 updatePausePlay(); |
| 213 } |
| 214 |
| 215 private String stringForTime(int timeMs) { |
| 216 int totalSeconds = timeMs / 1000; |
| 217 |
| 218 int seconds = totalSeconds % 60; |
| 219 int minutes = (totalSeconds / 60) % 60; |
| 220 int hours = totalSeconds / 3600; |
| 221 |
| 222 mFormatBuilder.setLength(0); |
| 223 if (hours > 0) { |
| 224 return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).to
String(); |
| 225 } else { |
| 226 return mFormatter.format("%02d:%02d", minutes, seconds).toString(); |
| 227 } |
| 228 } |
| 229 |
| 230 public long updateProgress() { |
| 231 if (mDelegate == null || mDragging) return 0; |
| 232 |
| 233 long position = mDelegate.getPosition(); |
| 234 long duration = mDelegate.getDuration(); |
| 235 if (duration <= 0) { |
| 236 // If there is no valid duration, hide the progress bar and time ind
icators. |
| 237 if (mProgressGroup != null) mProgressGroup.setVisibility(View.INVISI
BLE); |
| 238 } else if (mProgressBar != null) { |
| 239 if (mProgressGroup != null) mProgressGroup.setVisibility(View.VISIBL
E); |
| 240 // use long to avoid overflow |
| 241 long pos = 1000L * position / duration; |
| 242 mProgressBar.setProgress((int) pos); |
| 243 mProgressBar.setSecondaryProgress((int) pos); |
| 244 } |
| 245 |
| 246 if (mEndTime != null) mEndTime.setText(stringForTime((int) duration)); |
| 247 if (mCurrentTime != null) mCurrentTime.setText(stringForTime((int) posit
ion)); |
| 248 |
| 249 return position; |
| 250 } |
| 251 |
| 252 private View.OnClickListener mPauseListener = new View.OnClickListener() { |
| 253 @Override |
| 254 public void onClick(View v) { |
| 255 doPauseResume(); |
| 256 } |
| 257 }; |
| 258 |
| 259 private void updatePausePlay() { |
| 260 if (mDelegate == null || mPauseButton == null) return; |
| 261 |
| 262 if (mDelegate.isPlaying()) { |
| 263 mPauseButton.setImageResource(android.R.drawable.ic_media_pause); |
| 264 } else { |
| 265 mPauseButton.setImageResource(android.R.drawable.ic_media_play); |
| 266 } |
| 267 } |
| 268 |
| 269 private void doPauseResume() { |
| 270 if (mDelegate == null) return; |
| 271 |
| 272 if (mDelegate.isPlaying()) { |
| 273 mDelegate.pause(); |
| 274 } else { |
| 275 mDelegate.play(); |
| 276 } |
| 277 updatePausePlay(); |
| 278 } |
| 279 |
| 280 // There are two scenarios that can trigger the seekbar listener to trigger: |
| 281 // |
| 282 // The first is the user using the touchpad to adjust the posititon of the |
| 283 // seekbar's thumb. In this case onStartTrackingTouch is called followed by |
| 284 // a number of onProgressChanged notifications, concluded by onStopTrackingT
ouch. |
| 285 // We're setting the field "mDragging" to true for the duration of the dragg
ing |
| 286 // session to avoid jumps in the position in case of ongoing playback. |
| 287 // |
| 288 // The second scenario involves the user operating the scroll ball, in this |
| 289 // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notification
s, |
| 290 // we will simply apply the updated position without suspending regular upda
tes. |
| 291 private SeekBar.OnSeekBarChangeListener mSeekListener = new SeekBar.OnSeekBa
rChangeListener() { |
| 292 @Override |
| 293 public void onStartTrackingTouch(SeekBar bar) { |
| 294 mDragging = true; |
| 295 } |
| 296 |
| 297 @Override |
| 298 public void onProgressChanged(SeekBar bar, int progress, boolean fromuse
r) { |
| 299 if (mDelegate == null) return; |
| 300 |
| 301 if (!fromuser) { |
| 302 // We're not interested in programmatically generated changes to |
| 303 // the progress bar's position. |
| 304 return; |
| 305 } |
| 306 |
| 307 long duration = mDelegate.getDuration(); |
| 308 long newposition = (duration * progress) / 1000L; |
| 309 mDelegate.seekTo(newposition); |
| 310 if (mCurrentTime != null) mCurrentTime.setText(stringForTime((int) n
ewposition)); |
| 311 } |
| 312 |
| 313 @Override |
| 314 public void onStopTrackingTouch(SeekBar bar) { |
| 315 mDragging = false; |
| 316 updateProgress(); |
| 317 updatePausePlay(); |
| 318 } |
| 319 }; |
| 320 |
| 321 @Override |
| 322 public void setEnabled(boolean enabled) { |
| 323 super.setEnabled(enabled); |
| 324 updateButtons(); |
| 325 } |
| 326 |
| 327 @Override |
| 328 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { |
| 329 super.onInitializeAccessibilityEvent(event); |
| 330 event.setClassName(MediaController.class.getName()); |
| 331 } |
| 332 |
| 333 @Override |
| 334 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { |
| 335 super.onInitializeAccessibilityNodeInfo(info); |
| 336 info.setClassName(MediaController.class.getName()); |
| 337 } |
| 338 |
| 339 private View.OnClickListener mRewListener = new View.OnClickListener() { |
| 340 @Override |
| 341 public void onClick(View v) { |
| 342 if (mDelegate == null) return; |
| 343 |
| 344 long pos = mDelegate.getPosition(); |
| 345 pos -= 5000; // milliseconds |
| 346 mDelegate.seekTo(pos); |
| 347 updateProgress(); |
| 348 } |
| 349 }; |
| 350 |
| 351 private View.OnClickListener mFfwdListener = new View.OnClickListener() { |
| 352 @Override |
| 353 public void onClick(View v) { |
| 354 if (mDelegate == null) return; |
| 355 |
| 356 long pos = mDelegate.getPosition(); |
| 357 pos += 15000; // milliseconds |
| 358 mDelegate.seekTo(pos); |
| 359 updateProgress(); |
| 360 } |
| 361 }; |
| 362 |
| 363 private void installPrevNextListeners() { |
| 364 if (mNextButton != null) { |
| 365 mNextButton.setOnClickListener(mNextListener); |
| 366 mNextButton.setEnabled(mShowNext); |
| 367 } |
| 368 |
| 369 if (mPrevButton != null) { |
| 370 mPrevButton.setOnClickListener(mPrevListener); |
| 371 mPrevButton.setEnabled(mShowPrev); |
| 372 } |
| 373 } |
| 374 |
| 375 public void setPrevNextListeners(View.OnClickListener next, View.OnClickList
ener prev) { |
| 376 mNextListener = next; |
| 377 mPrevListener = prev; |
| 378 mListenersSet = true; |
| 379 |
| 380 installPrevNextListeners(); |
| 381 |
| 382 if (mNextButton != null) { |
| 383 mNextButton.setVisibility(View.VISIBLE); |
| 384 mShowNext = true; |
| 385 } |
| 386 if (mPrevButton != null) { |
| 387 mPrevButton.setVisibility(View.VISIBLE); |
| 388 mShowPrev = true; |
| 389 } |
| 390 } |
| 391 } |
OLD | NEW |