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 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 {
     68         Context getDataSourceContext();
     69         void runOnUiThread(Runnable runnable);
     70         void setStartStopListener(View.OnClickListener listener);
     71         void setPositionSeekListener(SeekBar.OnSeekBarChangeListener listener);
     72         void setSpeakerphoneListener(View.OnClickListener listener);
     73         void setIsBuffering();
     74         void setClipPosition(int clipPositionInMillis, int clipLengthInMillis);
     75         int getDesiredClipPosition();
     76         void playbackStarted();
     77         void playbackStopped();
     78         void playbackError(Exception e);
     79         boolean isSpeakerPhoneOn();
     80         void setSpeakerPhoneOn(boolean on);
     81         void finish();
     82         void setRateDisplay(float rate, int stringResourceId);
     83         void setRateIncreaseButtonListener(View.OnClickListener listener);
     84         void setRateDecreaseButtonListener(View.OnClickListener listener);
     85         void setIsFetchingContent();
     86         void disableUiElements();
     87         void enableUiElements();
     88         void sendFetchVoicemailRequest(Uri voicemailUri);
     89         boolean queryHasContent(Uri voicemailUri);
     90         void setFetchContentTimeout();
     91         void registerContentObserver(Uri uri, ContentObserver observer);
     92         void unregisterContentObserver(ContentObserver observer);
     93         void enableProximitySensor();
     94         void disableProximitySensor();
     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 
    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 
    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      */
    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      */
    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 
    249         public FetchResultHandler(Handler handler) {
    250             super(handler);
    251             mHandler = handler;
    252         }
    253 
    254         public Runnable getTimeoutRunnable() {
    255             return this;
    256         }
    257 
    258         @Override
    259         public void run() {
    260             if (mResultStillPending.getAndSet(false)) {
    261                 mView.unregisterContentObserver(FetchResultHandler.this);
    262                 mView.setFetchContentTimeout();
    263             }
    264         }
    265 
    266         public void destroy() {
    267             if (mResultStillPending.getAndSet(false)) {
    268                 mView.unregisterContentObserver(FetchResultHandler.this);
    269                 mHandler.removeCallbacks(this);
    270             }
    271         }
    272 
    273         @Override
    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      */
    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      */
    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 
    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         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
    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
    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 
    404     public View.OnClickListener createRateDecreaseListener() {
    405         return new RateChangeListener(false);
    406     }
    407 
    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 
    420         public RateChangeListener(boolean increase) {
    421             mIncrease = increase;
    422         }
    423 
    424         @Override
    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 
    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 
    480     private void handleError(Exception e) {
    481         mView.playbackError(e);
    482         mPositionUpdater.stopUpdating();
    483         mPlayer.release();
    484     }
    485 
    486     public void handleCompletion(MediaPlayer mediaPlayer) {
    487         stopPlaybackAtPosition(0, mDuration.get());
    488     }
    489 
    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
    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
    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
    528         public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    529             mView.setClipPosition(seekBar.getProgress(), seekBar.getMax());
    530         }
    531     }
    532 
    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
    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
    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 
    589         public PositionUpdater(ScheduledExecutorService executorService, int periodMillis) {
    590             mExecutorService = executorService;
    591             mPeriodMillis = periodMillis;
    592         }
    593 
    594         @Override
    595         public void run() {
    596             mView.runOnUiThread(mSetClipPostitionRunnable);
    597         }
    598 
    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 
    609         public void stopUpdating() {
    610             synchronized (mLock) {
    611                 if (mScheduledFuture != null) {
    612                     mScheduledFuture.cancel(false);
    613                     mScheduledFuture = null;
    614                 }
    615             }
    616         }
    617     }
    618 
    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