• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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