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