1 /* 2 * Copyright (C) 2011 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 com.android.dialer.voicemail; 18 19 import android.content.Context; 20 import android.database.ContentObserver; 21 import android.media.AudioManager; 22 import android.media.MediaPlayer; 23 import android.net.Uri; 24 import android.os.AsyncTask; 25 import android.os.Bundle; 26 import android.os.Handler; 27 import android.os.PowerManager; 28 import android.view.View; 29 import android.widget.SeekBar; 30 31 import com.android.dialer.R; 32 import com.android.dialer.util.AsyncTaskExecutor; 33 import com.android.ex.variablespeed.MediaPlayerProxy; 34 import com.android.ex.variablespeed.SingleThreadedMediaPlayerProxy; 35 import com.google.common.annotations.VisibleForTesting; 36 import com.google.common.base.Preconditions; 37 38 import java.util.concurrent.RejectedExecutionException; 39 import java.util.concurrent.ScheduledExecutorService; 40 import java.util.concurrent.ScheduledFuture; 41 import java.util.concurrent.TimeUnit; 42 import java.util.concurrent.atomic.AtomicBoolean; 43 import java.util.concurrent.atomic.AtomicInteger; 44 45 import javax.annotation.concurrent.GuardedBy; 46 import javax.annotation.concurrent.NotThreadSafe; 47 import javax.annotation.concurrent.ThreadSafe; 48 49 /** 50 * Contains the controlling logic for a voicemail playback ui. 51 * <p> 52 * Specifically right now this class is used to control the 53 * {@link com.android.dialer.voicemail.VoicemailPlaybackFragment}. 54 * <p> 55 * This class is not thread safe. The thread policy for this class is 56 * thread-confinement, all calls into this class from outside must be done from 57 * the main ui thread. 58 */ 59 @NotThreadSafe 60 @VisibleForTesting 61 public class VoicemailPlaybackPresenter { 62 /** The stream used to playback voicemail. */ 63 private static final int PLAYBACK_STREAM = AudioManager.STREAM_VOICE_CALL; 64 65 /** Contract describing the behaviour we need from the ui we are controlling. */ 66 public interface PlaybackView { getDataSourceContext()67 Context getDataSourceContext(); runOnUiThread(Runnable runnable)68 void runOnUiThread(Runnable runnable); setStartStopListener(View.OnClickListener listener)69 void setStartStopListener(View.OnClickListener listener); setPositionSeekListener(SeekBar.OnSeekBarChangeListener listener)70 void setPositionSeekListener(SeekBar.OnSeekBarChangeListener listener); setSpeakerphoneListener(View.OnClickListener listener)71 void setSpeakerphoneListener(View.OnClickListener listener); setIsBuffering()72 void setIsBuffering(); setClipPosition(int clipPositionInMillis, int clipLengthInMillis)73 void setClipPosition(int clipPositionInMillis, int clipLengthInMillis); getDesiredClipPosition()74 int getDesiredClipPosition(); playbackStarted()75 void playbackStarted(); playbackStopped()76 void playbackStopped(); playbackError(Exception e)77 void playbackError(Exception e); isSpeakerPhoneOn()78 boolean isSpeakerPhoneOn(); setSpeakerPhoneOn(boolean on)79 void setSpeakerPhoneOn(boolean on); finish()80 void finish(); setRateDisplay(float rate, int stringResourceId)81 void setRateDisplay(float rate, int stringResourceId); setRateIncreaseButtonListener(View.OnClickListener listener)82 void setRateIncreaseButtonListener(View.OnClickListener listener); setRateDecreaseButtonListener(View.OnClickListener listener)83 void setRateDecreaseButtonListener(View.OnClickListener listener); setIsFetchingContent()84 void setIsFetchingContent(); disableUiElements()85 void disableUiElements(); enableUiElements()86 void enableUiElements(); sendFetchVoicemailRequest(Uri voicemailUri)87 void sendFetchVoicemailRequest(Uri voicemailUri); queryHasContent(Uri voicemailUri)88 boolean queryHasContent(Uri voicemailUri); setFetchContentTimeout()89 void setFetchContentTimeout(); registerContentObserver(Uri uri, ContentObserver observer)90 void registerContentObserver(Uri uri, ContentObserver observer); unregisterContentObserver(ContentObserver observer)91 void unregisterContentObserver(ContentObserver observer); enableProximitySensor()92 void enableProximitySensor(); disableProximitySensor()93 void disableProximitySensor(); setVolumeControlStream(int streamType)94 void setVolumeControlStream(int streamType); 95 } 96 97 /** The enumeration of {@link AsyncTask} objects we use in this class. */ 98 public enum Tasks { 99 CHECK_FOR_CONTENT, 100 CHECK_CONTENT_AFTER_CHANGE, 101 PREPARE_MEDIA_PLAYER, 102 RESET_PREPARE_START_MEDIA_PLAYER, 103 } 104 105 /** Update rate for the slider, 30fps. */ 106 private static final int SLIDER_UPDATE_PERIOD_MILLIS = 1000 / 30; 107 /** Time our ui will wait for content to be fetched before reporting not available. */ 108 private static final long FETCH_CONTENT_TIMEOUT_MS = 20000; 109 /** 110 * If present in the saved instance bundle, we should not resume playback on 111 * create. 112 */ 113 private static final String PAUSED_STATE_KEY = VoicemailPlaybackPresenter.class.getName() 114 + ".PAUSED_STATE_KEY"; 115 /** 116 * If present in the saved instance bundle, indicates where to set the 117 * playback slider. 118 */ 119 private static final String CLIP_POSITION_KEY = VoicemailPlaybackPresenter.class.getName() 120 + ".CLIP_POSITION_KEY"; 121 122 /** The preset variable-speed rates. Each is greater than the previous by 25%. */ 123 private static final float[] PRESET_RATES = new float[] { 124 0.64f, 0.8f, 1.0f, 1.25f, 1.5625f 125 }; 126 /** The string resource ids corresponding to the names given to the above preset rates. */ 127 private static final int[] PRESET_NAMES = new int[] { 128 R.string.voicemail_speed_slowest, 129 R.string.voicemail_speed_slower, 130 R.string.voicemail_speed_normal, 131 R.string.voicemail_speed_faster, 132 R.string.voicemail_speed_fastest, 133 }; 134 135 /** 136 * Pointer into the {@link VoicemailPlaybackPresenter#PRESET_RATES} array. 137 * <p> 138 * This doesn't need to be synchronized, it's used only by the {@link RateChangeListener} 139 * which in turn is only executed on the ui thread. This can't be encapsulated inside the 140 * rate change listener since multiple rate change listeners must share the same value. 141 */ 142 private int mRateIndex = 2; 143 144 /** 145 * The most recently calculated duration. 146 * <p> 147 * We cache this in a field since we don't want to keep requesting it from the player, as 148 * this can easily lead to throwing {@link IllegalStateException} (any time the player is 149 * released, it's illegal to ask for the duration). 150 */ 151 private final AtomicInteger mDuration = new AtomicInteger(0); 152 153 private final PlaybackView mView; 154 private final MediaPlayerProxy mPlayer; 155 private final PositionUpdater mPositionUpdater; 156 157 /** Voicemail uri to play. */ 158 private final Uri mVoicemailUri; 159 /** Start playing in onCreate iff this is true. */ 160 private final boolean mStartPlayingImmediately; 161 /** Used to run async tasks that need to interact with the ui. */ 162 private final AsyncTaskExecutor mAsyncTaskExecutor; 163 164 /** 165 * Used to handle the result of a successful or time-out fetch result. 166 * <p> 167 * This variable is thread-contained, accessed only on the ui thread. 168 */ 169 private FetchResultHandler mFetchResultHandler; 170 private PowerManager.WakeLock mWakeLock; 171 private AsyncTask<Void, ?, ?> mPrepareTask; 172 VoicemailPlaybackPresenter(PlaybackView view, MediaPlayerProxy player, Uri voicemailUri, ScheduledExecutorService executorService, boolean startPlayingImmediately, AsyncTaskExecutor asyncTaskExecutor, PowerManager.WakeLock wakeLock)173 public VoicemailPlaybackPresenter(PlaybackView view, MediaPlayerProxy player, 174 Uri voicemailUri, ScheduledExecutorService executorService, 175 boolean startPlayingImmediately, AsyncTaskExecutor asyncTaskExecutor, 176 PowerManager.WakeLock wakeLock) { 177 mView = view; 178 mPlayer = player; 179 mVoicemailUri = voicemailUri; 180 mStartPlayingImmediately = startPlayingImmediately; 181 mAsyncTaskExecutor = asyncTaskExecutor; 182 mPositionUpdater = new PositionUpdater(executorService, SLIDER_UPDATE_PERIOD_MILLIS); 183 mWakeLock = wakeLock; 184 } 185 onCreate(Bundle bundle)186 public void onCreate(Bundle bundle) { 187 mView.setVolumeControlStream(PLAYBACK_STREAM); 188 checkThatWeHaveContent(); 189 } 190 191 /** 192 * Checks to see if we have content available for this voicemail. 193 * <p> 194 * This method will be called once, after the fragment has been created, before we know if the 195 * voicemail we've been asked to play has any content available. 196 * <p> 197 * This method will notify the user through the ui that we are fetching the content, then check 198 * to see if the content field in the db is set. If set, we proceed to 199 * {@link #postSuccessfullyFetchedContent()} method. If not set, we will make a request to fetch 200 * the content asynchronously via {@link #makeRequestForContent()}. 201 */ checkThatWeHaveContent()202 private void checkThatWeHaveContent() { 203 mView.setIsFetchingContent(); 204 mAsyncTaskExecutor.submit(Tasks.CHECK_FOR_CONTENT, new AsyncTask<Void, Void, Boolean>() { 205 @Override 206 public Boolean doInBackground(Void... params) { 207 return mView.queryHasContent(mVoicemailUri); 208 } 209 210 @Override 211 public void onPostExecute(Boolean hasContent) { 212 if (hasContent) { 213 postSuccessfullyFetchedContent(); 214 } else { 215 makeRequestForContent(); 216 } 217 } 218 }); 219 } 220 221 /** 222 * Makes a broadcast request to ask that a voicemail source fetch this content. 223 * <p> 224 * This method <b>must be called on the ui thread</b>. 225 * <p> 226 * This method will be called when we realise that we don't have content for this voicemail. It 227 * will trigger a broadcast to request that the content be downloaded. It will add a listener to 228 * the content resolver so that it will be notified when the has_content field changes. It will 229 * also set a timer. If the has_content field changes to true within the allowed time, we will 230 * proceed to {@link #postSuccessfullyFetchedContent()}. If the has_content field does not 231 * become true within the allowed time, we will update the ui to reflect the fact that content 232 * was not available. 233 */ makeRequestForContent()234 private void makeRequestForContent() { 235 Handler handler = new Handler(); 236 Preconditions.checkState(mFetchResultHandler == null, "mFetchResultHandler should be null"); 237 mFetchResultHandler = new FetchResultHandler(handler); 238 mView.registerContentObserver(mVoicemailUri, mFetchResultHandler); 239 handler.postDelayed(mFetchResultHandler.getTimeoutRunnable(), FETCH_CONTENT_TIMEOUT_MS); 240 mView.sendFetchVoicemailRequest(mVoicemailUri); 241 } 242 243 @ThreadSafe 244 private class FetchResultHandler extends ContentObserver implements Runnable { 245 private AtomicBoolean mResultStillPending = new AtomicBoolean(true); 246 private final Handler mHandler; 247 FetchResultHandler(Handler handler)248 public FetchResultHandler(Handler handler) { 249 super(handler); 250 mHandler = handler; 251 } 252 getTimeoutRunnable()253 public Runnable getTimeoutRunnable() { 254 return this; 255 } 256 257 @Override run()258 public void run() { 259 if (mResultStillPending.getAndSet(false)) { 260 mView.unregisterContentObserver(FetchResultHandler.this); 261 mView.setFetchContentTimeout(); 262 } 263 } 264 destroy()265 public void destroy() { 266 if (mResultStillPending.getAndSet(false)) { 267 mView.unregisterContentObserver(FetchResultHandler.this); 268 mHandler.removeCallbacks(this); 269 } 270 } 271 272 @Override onChange(boolean selfChange)273 public void onChange(boolean selfChange) { 274 mAsyncTaskExecutor.submit(Tasks.CHECK_CONTENT_AFTER_CHANGE, 275 new AsyncTask<Void, Void, Boolean>() { 276 @Override 277 public Boolean doInBackground(Void... params) { 278 return mView.queryHasContent(mVoicemailUri); 279 } 280 281 @Override 282 public void onPostExecute(Boolean hasContent) { 283 if (hasContent) { 284 if (mResultStillPending.getAndSet(false)) { 285 mView.unregisterContentObserver(FetchResultHandler.this); 286 postSuccessfullyFetchedContent(); 287 } 288 } 289 } 290 }); 291 } 292 } 293 294 /** 295 * Prepares the voicemail content for playback. 296 * <p> 297 * This method will be called once we know that our voicemail has content (according to the 298 * content provider). This method will try to prepare the data source through the media player. 299 * If preparing the media player works, we will call through to 300 * {@link #postSuccessfulPrepareActions()}. If preparing the media player fails (perhaps the 301 * file the content provider points to is actually missing, perhaps it is of an unknown file 302 * format that we can't play, who knows) then we will show an error on the ui. 303 */ postSuccessfullyFetchedContent()304 private void postSuccessfullyFetchedContent() { 305 mView.setIsBuffering(); 306 mAsyncTaskExecutor.submit(Tasks.PREPARE_MEDIA_PLAYER, 307 new AsyncTask<Void, Void, Exception>() { 308 @Override 309 public Exception doInBackground(Void... params) { 310 try { 311 mPlayer.reset(); 312 mPlayer.setDataSource(mView.getDataSourceContext(), mVoicemailUri); 313 mPlayer.setAudioStreamType(PLAYBACK_STREAM); 314 mPlayer.prepare(); 315 mDuration.set(mPlayer.getDuration()); 316 return null; 317 } catch (Exception e) { 318 return e; 319 } 320 } 321 322 @Override 323 public void onPostExecute(Exception exception) { 324 if (exception == null) { 325 postSuccessfulPrepareActions(); 326 } else { 327 mView.playbackError(exception); 328 } 329 } 330 }); 331 } 332 333 /** 334 * Enables the ui, and optionally starts playback immediately. 335 * <p> 336 * This will be called once we have successfully prepared the media player, and will optionally 337 * playback immediately. 338 */ postSuccessfulPrepareActions()339 private void postSuccessfulPrepareActions() { 340 mView.enableUiElements(); 341 mView.setPositionSeekListener(new PlaybackPositionListener()); 342 mView.setStartStopListener(new StartStopButtonListener()); 343 mView.setSpeakerphoneListener(new SpeakerphoneListener()); 344 mPlayer.setOnErrorListener(new MediaPlayerErrorListener()); 345 mPlayer.setOnCompletionListener(new MediaPlayerCompletionListener()); 346 mView.setSpeakerPhoneOn(mView.isSpeakerPhoneOn()); 347 mView.setRateDecreaseButtonListener(createRateDecreaseListener()); 348 mView.setRateIncreaseButtonListener(createRateIncreaseListener()); 349 mView.setClipPosition(0, mDuration.get()); 350 mView.playbackStopped(); 351 // Always disable on stop. 352 mView.disableProximitySensor(); 353 if (mStartPlayingImmediately) { 354 resetPrepareStartPlaying(0); 355 } 356 // TODO: Now I'm ignoring the bundle, when previously I was checking for contains against 357 // the PAUSED_STATE_KEY, and CLIP_POSITION_KEY. 358 } 359 onSaveInstanceState(Bundle outState)360 public void onSaveInstanceState(Bundle outState) { 361 outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition()); 362 if (!mPlayer.isPlaying()) { 363 outState.putBoolean(PAUSED_STATE_KEY, true); 364 } 365 } 366 onDestroy()367 public void onDestroy() { 368 if (mPrepareTask != null) { 369 mPrepareTask.cancel(false); 370 mPrepareTask = null; 371 } 372 mPlayer.release(); 373 if (mFetchResultHandler != null) { 374 mFetchResultHandler.destroy(); 375 mFetchResultHandler = null; 376 } 377 mPositionUpdater.stopUpdating(); 378 if (mWakeLock.isHeld()) { 379 mWakeLock.release(); 380 } 381 } 382 383 private class MediaPlayerErrorListener implements MediaPlayer.OnErrorListener { 384 @Override onError(MediaPlayer mp, int what, int extra)385 public boolean onError(MediaPlayer mp, int what, int extra) { 386 mView.runOnUiThread(new Runnable() { 387 @Override 388 public void run() { 389 handleError(new IllegalStateException("MediaPlayer error listener invoked")); 390 } 391 }); 392 return true; 393 } 394 } 395 396 private class MediaPlayerCompletionListener implements MediaPlayer.OnCompletionListener { 397 @Override onCompletion(final MediaPlayer mp)398 public void onCompletion(final MediaPlayer mp) { 399 mView.runOnUiThread(new Runnable() { 400 @Override 401 public void run() { 402 handleCompletion(mp); 403 } 404 }); 405 } 406 } 407 createRateDecreaseListener()408 public View.OnClickListener createRateDecreaseListener() { 409 return new RateChangeListener(false); 410 } 411 createRateIncreaseListener()412 public View.OnClickListener createRateIncreaseListener() { 413 return new RateChangeListener(true); 414 } 415 416 /** 417 * Listens to clicks on the rate increase and decrease buttons. 418 * <p> 419 * This class is not thread-safe, but all interactions with it will happen on the ui thread. 420 */ 421 private class RateChangeListener implements View.OnClickListener { 422 private final boolean mIncrease; 423 RateChangeListener(boolean increase)424 public RateChangeListener(boolean increase) { 425 mIncrease = increase; 426 } 427 428 @Override onClick(View v)429 public void onClick(View v) { 430 // Adjust the current rate, then clamp it to the allowed values. 431 mRateIndex = constrain(mRateIndex + (mIncrease ? 1 : -1), 0, PRESET_RATES.length - 1); 432 // Whether or not we have actually changed the index, call changeRate(). 433 // This will ensure that we show the "fastest" or "slowest" text on the ui to indicate 434 // to the user that it doesn't get any faster or slower. 435 changeRate(PRESET_RATES[mRateIndex], PRESET_NAMES[mRateIndex]); 436 } 437 } 438 439 private class AsyncPrepareTask extends AsyncTask<Void, Void, Exception> { 440 private int mClipPositionInMillis; 441 AsyncPrepareTask(int clipPositionInMillis)442 AsyncPrepareTask(int clipPositionInMillis) { 443 mClipPositionInMillis = clipPositionInMillis; 444 } 445 446 @Override doInBackground(Void... params)447 public Exception doInBackground(Void... params) { 448 try { 449 mPlayer.reset(); 450 mPlayer.setDataSource(mView.getDataSourceContext(), mVoicemailUri); 451 mPlayer.setAudioStreamType(PLAYBACK_STREAM); 452 mPlayer.prepare(); 453 return null; 454 } catch (Exception e) { 455 return e; 456 } 457 } 458 459 @Override onPostExecute(Exception exception)460 public void onPostExecute(Exception exception) { 461 mPrepareTask = null; 462 if (exception == null) { 463 final int duration = mPlayer.getDuration(); 464 mDuration.set(duration); 465 int startPosition = 466 constrain(mClipPositionInMillis, 0, duration); 467 mPlayer.seekTo(startPosition); 468 mView.setClipPosition(startPosition, duration); 469 try { 470 // Can throw RejectedExecutionException 471 mPlayer.start(); 472 mView.playbackStarted(); 473 if (!mWakeLock.isHeld()) { 474 mWakeLock.acquire(); 475 } 476 // Only enable if we are not currently using the speaker phone. 477 if (!mView.isSpeakerPhoneOn()) { 478 mView.enableProximitySensor(); 479 } 480 // Can throw RejectedExecutionException 481 mPositionUpdater.startUpdating(startPosition, duration); 482 } catch (RejectedExecutionException e) { 483 handleError(e); 484 } 485 } else { 486 handleError(exception); 487 } 488 } 489 } 490 resetPrepareStartPlaying(final int clipPositionInMillis)491 private void resetPrepareStartPlaying(final int clipPositionInMillis) { 492 if (mPrepareTask != null) { 493 mPrepareTask.cancel(false); 494 mPrepareTask = null; 495 } 496 mPrepareTask = mAsyncTaskExecutor.submit(Tasks.RESET_PREPARE_START_MEDIA_PLAYER, 497 new AsyncPrepareTask(clipPositionInMillis)); 498 } 499 handleError(Exception e)500 private void handleError(Exception e) { 501 mView.playbackError(e); 502 mPositionUpdater.stopUpdating(); 503 mPlayer.release(); 504 } 505 handleCompletion(MediaPlayer mediaPlayer)506 public void handleCompletion(MediaPlayer mediaPlayer) { 507 stopPlaybackAtPosition(0, mDuration.get()); 508 } 509 stopPlaybackAtPosition(int clipPosition, int duration)510 private void stopPlaybackAtPosition(int clipPosition, int duration) { 511 mPositionUpdater.stopUpdating(); 512 mView.playbackStopped(); 513 if (mWakeLock.isHeld()) { 514 mWakeLock.release(); 515 } 516 // Always disable on stop. 517 mView.disableProximitySensor(); 518 mView.setClipPosition(clipPosition, duration); 519 if (mPlayer.isPlaying()) { 520 mPlayer.pause(); 521 } 522 } 523 524 private class PlaybackPositionListener implements SeekBar.OnSeekBarChangeListener { 525 private boolean mShouldResumePlaybackAfterSeeking; 526 527 @Override onStartTrackingTouch(SeekBar arg0)528 public void onStartTrackingTouch(SeekBar arg0) { 529 if (mPlayer.isPlaying()) { 530 mShouldResumePlaybackAfterSeeking = true; 531 stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get()); 532 } else { 533 mShouldResumePlaybackAfterSeeking = false; 534 } 535 } 536 537 @Override onStopTrackingTouch(SeekBar arg0)538 public void onStopTrackingTouch(SeekBar arg0) { 539 if (mPlayer.isPlaying()) { 540 stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get()); 541 } 542 if (mShouldResumePlaybackAfterSeeking) { 543 resetPrepareStartPlaying(mView.getDesiredClipPosition()); 544 } 545 } 546 547 @Override onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)548 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 549 mView.setClipPosition(seekBar.getProgress(), seekBar.getMax()); 550 } 551 } 552 changeRate(float rate, int stringResourceId)553 private void changeRate(float rate, int stringResourceId) { 554 ((SingleThreadedMediaPlayerProxy) mPlayer).setVariableSpeed(rate); 555 mView.setRateDisplay(rate, stringResourceId); 556 } 557 558 private class SpeakerphoneListener implements View.OnClickListener { 559 @Override onClick(View v)560 public void onClick(View v) { 561 boolean previousState = mView.isSpeakerPhoneOn(); 562 mView.setSpeakerPhoneOn(!previousState); 563 if (mPlayer.isPlaying() && previousState) { 564 // If we are currently playing and we are disabling the speaker phone, enable the 565 // sensor. 566 mView.enableProximitySensor(); 567 } else { 568 // If we are not currently playing, disable the sensor. 569 mView.disableProximitySensor(); 570 } 571 } 572 } 573 574 private class StartStopButtonListener implements View.OnClickListener { 575 @Override onClick(View arg0)576 public void onClick(View arg0) { 577 if (mPlayer.isPlaying()) { 578 stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get()); 579 } else { 580 resetPrepareStartPlaying(mView.getDesiredClipPosition()); 581 } 582 } 583 } 584 585 /** 586 * Controls the animation of the playback slider. 587 */ 588 @ThreadSafe 589 private final class PositionUpdater implements Runnable { 590 private final ScheduledExecutorService mExecutorService; 591 private final int mPeriodMillis; 592 private final Object mLock = new Object(); 593 @GuardedBy("mLock") private ScheduledFuture<?> mScheduledFuture; 594 private final Runnable mSetClipPostitionRunnable = new Runnable() { 595 @Override 596 public void run() { 597 int currentPosition = 0; 598 synchronized (mLock) { 599 if (mScheduledFuture == null) { 600 // This task has been canceled. Just stop now. 601 return; 602 } 603 currentPosition = mPlayer.getCurrentPosition(); 604 } 605 mView.setClipPosition(currentPosition, mDuration.get()); 606 } 607 }; 608 PositionUpdater(ScheduledExecutorService executorService, int periodMillis)609 public PositionUpdater(ScheduledExecutorService executorService, int periodMillis) { 610 mExecutorService = executorService; 611 mPeriodMillis = periodMillis; 612 } 613 614 @Override run()615 public void run() { 616 mView.runOnUiThread(mSetClipPostitionRunnable); 617 } 618 startUpdating(int beginPosition, int endPosition)619 public void startUpdating(int beginPosition, int endPosition) { 620 synchronized (mLock) { 621 if (mScheduledFuture != null) { 622 mScheduledFuture.cancel(false); 623 mScheduledFuture = null; 624 } 625 mScheduledFuture = mExecutorService.scheduleAtFixedRate(this, 0, mPeriodMillis, 626 TimeUnit.MILLISECONDS); 627 } 628 } 629 stopUpdating()630 public void stopUpdating() { 631 synchronized (mLock) { 632 if (mScheduledFuture != null) { 633 mScheduledFuture.cancel(false); 634 mScheduledFuture = null; 635 } 636 } 637 } 638 } 639 onPause()640 public void onPause() { 641 if (mPlayer.isPlaying()) { 642 stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get()); 643 } 644 if (mPrepareTask != null) { 645 mPrepareTask.cancel(false); 646 mPrepareTask = null; 647 } 648 if (mWakeLock.isHeld()) { 649 mWakeLock.release(); 650 } 651 } 652 constrain(int amount, int low, int high)653 private static int constrain(int amount, int low, int high) { 654 return amount < low ? low : (amount > high ? high : amount); 655 } 656 } 657