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