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 if (!mPlayer.isReadyToPlay()) { 450 mPlayer.reset(); 451 mPlayer.setDataSource(mView.getDataSourceContext(), mVoicemailUri); 452 mPlayer.setAudioStreamType(PLAYBACK_STREAM); 453 mPlayer.prepare(); 454 } 455 return null; 456 } catch (Exception e) { 457 return e; 458 } 459 } 460 461 @Override onPostExecute(Exception exception)462 public void onPostExecute(Exception exception) { 463 mPrepareTask = null; 464 if (exception == null) { 465 final int duration = mPlayer.getDuration(); 466 mDuration.set(duration); 467 int startPosition = 468 constrain(mClipPositionInMillis, 0, duration); 469 mPlayer.seekTo(startPosition); 470 mView.setClipPosition(startPosition, duration); 471 try { 472 // Can throw RejectedExecutionException 473 mPlayer.start(); 474 mView.playbackStarted(); 475 if (!mWakeLock.isHeld()) { 476 mWakeLock.acquire(); 477 } 478 // Only enable if we are not currently using the speaker phone. 479 if (!mView.isSpeakerPhoneOn()) { 480 mView.enableProximitySensor(); 481 } 482 // Can throw RejectedExecutionException 483 mPositionUpdater.startUpdating(startPosition, duration); 484 } catch (RejectedExecutionException e) { 485 handleError(e); 486 } 487 } else { 488 handleError(exception); 489 } 490 } 491 } 492 resetPrepareStartPlaying(final int clipPositionInMillis)493 private void resetPrepareStartPlaying(final int clipPositionInMillis) { 494 if (mPrepareTask != null) { 495 mPrepareTask.cancel(false); 496 mPrepareTask = null; 497 } 498 mPrepareTask = mAsyncTaskExecutor.submit(Tasks.RESET_PREPARE_START_MEDIA_PLAYER, 499 new AsyncPrepareTask(clipPositionInMillis)); 500 } 501 handleError(Exception e)502 private void handleError(Exception e) { 503 mView.playbackError(e); 504 mPositionUpdater.stopUpdating(); 505 mPlayer.release(); 506 } 507 handleCompletion(MediaPlayer mediaPlayer)508 public void handleCompletion(MediaPlayer mediaPlayer) { 509 stopPlaybackAtPosition(0, mDuration.get()); 510 } 511 stopPlaybackAtPosition(int clipPosition, int duration)512 private void stopPlaybackAtPosition(int clipPosition, int duration) { 513 mPositionUpdater.stopUpdating(); 514 mView.playbackStopped(); 515 if (mWakeLock.isHeld()) { 516 mWakeLock.release(); 517 } 518 // Always disable on stop. 519 mView.disableProximitySensor(); 520 mView.setClipPosition(clipPosition, duration); 521 if (mPlayer.isPlaying()) { 522 mPlayer.pause(); 523 } 524 } 525 526 private class PlaybackPositionListener implements SeekBar.OnSeekBarChangeListener { 527 private boolean mShouldResumePlaybackAfterSeeking; 528 529 @Override onStartTrackingTouch(SeekBar arg0)530 public void onStartTrackingTouch(SeekBar arg0) { 531 if (mPlayer.isPlaying()) { 532 mShouldResumePlaybackAfterSeeking = true; 533 stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get()); 534 } else { 535 mShouldResumePlaybackAfterSeeking = false; 536 } 537 } 538 539 @Override onStopTrackingTouch(SeekBar arg0)540 public void onStopTrackingTouch(SeekBar arg0) { 541 if (mPlayer.isPlaying()) { 542 stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get()); 543 } 544 if (mShouldResumePlaybackAfterSeeking) { 545 resetPrepareStartPlaying(mView.getDesiredClipPosition()); 546 } 547 } 548 549 @Override onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)550 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 551 mView.setClipPosition(seekBar.getProgress(), seekBar.getMax()); 552 } 553 } 554 changeRate(float rate, int stringResourceId)555 private void changeRate(float rate, int stringResourceId) { 556 ((SingleThreadedMediaPlayerProxy) mPlayer).setVariableSpeed(rate); 557 mView.setRateDisplay(rate, stringResourceId); 558 } 559 560 private class SpeakerphoneListener implements View.OnClickListener { 561 @Override onClick(View v)562 public void onClick(View v) { 563 boolean previousState = mView.isSpeakerPhoneOn(); 564 mView.setSpeakerPhoneOn(!previousState); 565 if (mPlayer.isPlaying() && previousState) { 566 // If we are currently playing and we are disabling the speaker phone, enable the 567 // sensor. 568 mView.enableProximitySensor(); 569 } else { 570 // If we are not currently playing, disable the sensor. 571 mView.disableProximitySensor(); 572 } 573 } 574 } 575 576 private class StartStopButtonListener implements View.OnClickListener { 577 @Override onClick(View arg0)578 public void onClick(View arg0) { 579 if (mPlayer.isPlaying()) { 580 stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get()); 581 } else { 582 resetPrepareStartPlaying(mView.getDesiredClipPosition()); 583 } 584 } 585 } 586 587 /** 588 * Controls the animation of the playback slider. 589 */ 590 @ThreadSafe 591 private final class PositionUpdater implements Runnable { 592 private final ScheduledExecutorService mExecutorService; 593 private final int mPeriodMillis; 594 private final Object mLock = new Object(); 595 @GuardedBy("mLock") private ScheduledFuture<?> mScheduledFuture; 596 private final Runnable mSetClipPostitionRunnable = new Runnable() { 597 @Override 598 public void run() { 599 int currentPosition = 0; 600 synchronized (mLock) { 601 if (mScheduledFuture == null) { 602 // This task has been canceled. Just stop now. 603 return; 604 } 605 currentPosition = mPlayer.getCurrentPosition(); 606 } 607 mView.setClipPosition(currentPosition, mDuration.get()); 608 } 609 }; 610 PositionUpdater(ScheduledExecutorService executorService, int periodMillis)611 public PositionUpdater(ScheduledExecutorService executorService, int periodMillis) { 612 mExecutorService = executorService; 613 mPeriodMillis = periodMillis; 614 } 615 616 @Override run()617 public void run() { 618 mView.runOnUiThread(mSetClipPostitionRunnable); 619 } 620 startUpdating(int beginPosition, int endPosition)621 public void startUpdating(int beginPosition, int endPosition) { 622 synchronized (mLock) { 623 if (mScheduledFuture != null) { 624 mScheduledFuture.cancel(false); 625 mScheduledFuture = null; 626 } 627 mScheduledFuture = mExecutorService.scheduleAtFixedRate(this, 0, mPeriodMillis, 628 TimeUnit.MILLISECONDS); 629 } 630 } 631 stopUpdating()632 public void stopUpdating() { 633 synchronized (mLock) { 634 if (mScheduledFuture != null) { 635 mScheduledFuture.cancel(false); 636 mScheduledFuture = null; 637 } 638 } 639 } 640 } 641 onPause()642 public void onPause() { 643 if (mPlayer.isPlaying()) { 644 stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get()); 645 } 646 if (mPrepareTask != null) { 647 mPrepareTask.cancel(false); 648 mPrepareTask = null; 649 } 650 if (mWakeLock.isHeld()) { 651 mWakeLock.release(); 652 } 653 } 654 constrain(int amount, int low, int high)655 private static int constrain(int amount, int low, int high) { 656 return amount < low ? low : (amount > high ? high : amount); 657 } 658 } 659