Home | History | Annotate | Download | only in voicemail
      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 {
     67         Context getDataSourceContext();
     68         void runOnUiThread(Runnable runnable);
     69         void setStartStopListener(View.OnClickListener listener);
     70         void setPositionSeekListener(SeekBar.OnSeekBarChangeListener listener);
     71         void setSpeakerphoneListener(View.OnClickListener listener);
     72         void setIsBuffering();
     73         void setClipPosition(int clipPositionInMillis, int clipLengthInMillis);
     74         int getDesiredClipPosition();
     75         void playbackStarted();
     76         void playbackStopped();
     77         void playbackError(Exception e);
     78         boolean isSpeakerPhoneOn();
     79         void setSpeakerPhoneOn(boolean on);
     80         void finish();
     81         void setRateDisplay(float rate, int stringResourceId);
     82         void setRateIncreaseButtonListener(View.OnClickListener listener);
     83         void setRateDecreaseButtonListener(View.OnClickListener listener);
     84         void setIsFetchingContent();
     85         void disableUiElements();
     86         void enableUiElements();
     87         void sendFetchVoicemailRequest(Uri voicemailUri);
     88         boolean queryHasContent(Uri voicemailUri);
     89         void setFetchContentTimeout();
     90         void registerContentObserver(Uri uri, ContentObserver observer);
     91         void unregisterContentObserver(ContentObserver observer);
     92         void enableProximitySensor();
     93         void disableProximitySensor();
     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 
    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 
    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      */
    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      */
    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 
    248         public FetchResultHandler(Handler handler) {
    249             super(handler);
    250             mHandler = handler;
    251         }
    252 
    253         public Runnable getTimeoutRunnable() {
    254             return this;
    255         }
    256 
    257         @Override
    258         public void run() {
    259             if (mResultStillPending.getAndSet(false)) {
    260                 mView.unregisterContentObserver(FetchResultHandler.this);
    261                 mView.setFetchContentTimeout();
    262             }
    263         }
    264 
    265         public void destroy() {
    266             if (mResultStillPending.getAndSet(false)) {
    267                 mView.unregisterContentObserver(FetchResultHandler.this);
    268                 mHandler.removeCallbacks(this);
    269             }
    270         }
    271 
    272         @Override
    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      */
    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      */
    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 
    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 
    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
    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
    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 
    408     public View.OnClickListener createRateDecreaseListener() {
    409         return new RateChangeListener(false);
    410     }
    411 
    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 
    424         public RateChangeListener(boolean increase) {
    425             mIncrease = increase;
    426         }
    427 
    428         @Override
    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 
    442         AsyncPrepareTask(int clipPositionInMillis) {
    443             mClipPositionInMillis = clipPositionInMillis;
    444         }
    445 
    446         @Override
    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
    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 
    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 
    500     private void handleError(Exception e) {
    501         mView.playbackError(e);
    502         mPositionUpdater.stopUpdating();
    503         mPlayer.release();
    504     }
    505 
    506     public void handleCompletion(MediaPlayer mediaPlayer) {
    507         stopPlaybackAtPosition(0, mDuration.get());
    508     }
    509 
    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
    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
    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
    548         public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    549             mView.setClipPosition(seekBar.getProgress(), seekBar.getMax());
    550         }
    551     }
    552 
    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
    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
    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 
    609         public PositionUpdater(ScheduledExecutorService executorService, int periodMillis) {
    610             mExecutorService = executorService;
    611             mPeriodMillis = periodMillis;
    612         }
    613 
    614         @Override
    615         public void run() {
    616             mView.runOnUiThread(mSetClipPostitionRunnable);
    617         }
    618 
    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 
    630         public void stopUpdating() {
    631             synchronized (mLock) {
    632                 if (mScheduledFuture != null) {
    633                     mScheduledFuture.cancel(false);
    634                     mScheduledFuture = null;
    635                 }
    636             }
    637         }
    638     }
    639 
    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 
    653     private static int constrain(int amount, int low, int high) {
    654         return amount < low ? low : (amount > high ? high : amount);
    655     }
    656 }
    657