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.app.Activity;
     20 import android.content.Context;
     21 import android.content.ContentResolver;
     22 import android.content.Intent;
     23 import android.database.ContentObserver;
     24 import android.database.Cursor;
     25 import android.media.AudioManager;
     26 import android.media.AudioManager.OnAudioFocusChangeListener;
     27 import android.media.MediaPlayer;
     28 import android.net.Uri;
     29 import android.os.AsyncTask;
     30 import android.os.Bundle;
     31 import android.os.Handler;
     32 import android.os.PowerManager;
     33 import android.provider.VoicemailContract;
     34 import android.util.Log;
     35 import android.view.View;
     36 import android.view.WindowManager.LayoutParams;
     37 import android.widget.SeekBar;
     38 
     39 import com.android.dialer.R;
     40 import com.android.dialer.util.AsyncTaskExecutor;
     41 import com.android.dialer.util.AsyncTaskExecutors;
     42 
     43 import com.android.common.io.MoreCloseables;
     44 import com.google.common.annotations.VisibleForTesting;
     45 import com.google.common.base.Preconditions;
     46 
     47 import java.io.IOException;
     48 import java.util.concurrent.Executors;
     49 import java.util.concurrent.ScheduledExecutorService;
     50 import java.util.concurrent.RejectedExecutionException;
     51 import java.util.concurrent.ScheduledExecutorService;
     52 import java.util.concurrent.ScheduledFuture;
     53 import java.util.concurrent.atomic.AtomicBoolean;
     54 import java.util.concurrent.atomic.AtomicInteger;
     55 
     56 import javax.annotation.concurrent.NotThreadSafe;
     57 import javax.annotation.concurrent.ThreadSafe;
     58 
     59 /**
     60  * Contains the controlling logic for a voicemail playback in the call log. It is closely coupled
     61  * to assumptions about the behaviors and lifecycle of the call log, in particular in the
     62  * {@link CallLogFragment} and {@link CallLogAdapter}.
     63  * <p>
     64  * This controls a single {@link com.android.dialer.voicemail.VoicemailPlaybackLayout}. A single
     65  * instance can be reused for different such layouts, using {@link #setVoicemailPlaybackView}. This
     66  * is to facilitate reuse across different voicemail call log entries.
     67  * <p>
     68  * This class is not thread safe. The thread policy for this class is thread-confinement, all calls
     69  * into this class from outside must be done from the main UI thread.
     70  */
     71 @NotThreadSafe
     72 @VisibleForTesting
     73 public class VoicemailPlaybackPresenter
     74         implements OnAudioFocusChangeListener, MediaPlayer.OnPreparedListener,
     75                 MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener {
     76 
     77     private static final String TAG = VoicemailPlaybackPresenter.class.getSimpleName();
     78 
     79     /** Contract describing the behaviour we need from the ui we are controlling. */
     80     public interface PlaybackView {
     81         int getDesiredClipPosition();
     82         void disableUiElements();
     83         void enableUiElements();
     84         void onPlaybackError();
     85         void onPlaybackStarted(int duration, ScheduledExecutorService executorService);
     86         void onPlaybackStopped();
     87         void onSpeakerphoneOn(boolean on);
     88         void setClipPosition(int clipPositionInMillis, int clipLengthInMillis);
     89         void setFetchContentTimeout();
     90         void setIsBuffering();
     91         void setIsFetchingContent();
     92         void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri);
     93     }
     94 
     95     public interface OnVoicemailDeletedListener {
     96         void onVoicemailDeleted(Uri uri);
     97     }
     98 
     99     /** The enumeration of {@link AsyncTask} objects we use in this class. */
    100     public enum Tasks {
    101         CHECK_FOR_CONTENT,
    102         CHECK_CONTENT_AFTER_CHANGE,
    103     }
    104 
    105     private static final String[] HAS_CONTENT_PROJECTION = new String[] {
    106         VoicemailContract.Voicemails.HAS_CONTENT,
    107     };
    108 
    109     public static final int PLAYBACK_STREAM = AudioManager.STREAM_VOICE_CALL;
    110     private static final int NUMBER_OF_THREADS_IN_POOL = 2;
    111     // Time to wait for content to be fetched before timing out.
    112     private static final long FETCH_CONTENT_TIMEOUT_MS = 20000;
    113 
    114     private static final String VOICEMAIL_URI_KEY =
    115             VoicemailPlaybackPresenter.class.getName() + ".VOICEMAIL_URI";
    116     private static final String IS_PREPARED_KEY =
    117             VoicemailPlaybackPresenter.class.getName() + ".IS_PREPARED";
    118     // If present in the saved instance bundle, we should not resume playback on create.
    119     private static final String IS_PLAYING_STATE_KEY =
    120             VoicemailPlaybackPresenter.class.getName() + ".IS_PLAYING_STATE_KEY";
    121     // If present in the saved instance bundle, indicates where to set the playback slider.
    122     private static final String CLIP_POSITION_KEY =
    123             VoicemailPlaybackPresenter.class.getName() + ".CLIP_POSITION_KEY";
    124 
    125     /**
    126      * The most recently cached duration. We cache this since we don't want to keep requesting it
    127      * from the player, as this can easily lead to throwing {@link IllegalStateException} (any time
    128      * the player is released, it's illegal to ask for the duration).
    129      */
    130     private final AtomicInteger mDuration = new AtomicInteger(0);
    131 
    132     private static VoicemailPlaybackPresenter sInstance;
    133 
    134     private Activity mActivity;
    135     private Context mContext;
    136     private PlaybackView mView;
    137     private Uri mVoicemailUri;
    138 
    139     private MediaPlayer mMediaPlayer;
    140     private int mPosition;
    141     private boolean mIsPlaying;
    142     // MediaPlayer crashes on some method calls if not prepared but does not have a method which
    143     // exposes its prepared state. Store this locally, so we can check and prevent crashes.
    144     private boolean mIsPrepared;
    145 
    146     private boolean mShouldResumePlaybackAfterSeeking;
    147     private int mInitialOrientation;
    148 
    149     // Used to run async tasks that need to interact with the UI.
    150     private AsyncTaskExecutor mAsyncTaskExecutor;
    151     private static ScheduledExecutorService mScheduledExecutorService;
    152     /**
    153      * Used to handle the result of a successful or time-out fetch result.
    154      * <p>
    155      * This variable is thread-contained, accessed only on the ui thread.
    156      */
    157     private FetchResultHandler mFetchResultHandler;
    158     private Handler mHandler = new Handler();
    159     private PowerManager.WakeLock mProximityWakeLock;
    160     private AudioManager mAudioManager;
    161 
    162     private OnVoicemailDeletedListener mOnVoicemailDeletedListener;
    163 
    164     /**
    165      * Obtain singleton instance of this class. Use a single instance to provide a consistent
    166      * listener to the AudioManager when requesting and abandoning audio focus.
    167      *
    168      * Otherwise, after rotation the previous listener will still be active but a new listener
    169      * will be provided to calls to the AudioManager, which is bad. For example, abandoning
    170      * audio focus with the new listeners results in an AUDIO_FOCUS_GAIN callback to the
    171      * previous listener, which is the opposite of the intended behavior.
    172      */
    173     public static VoicemailPlaybackPresenter getInstance(
    174             Activity activity, Bundle savedInstanceState) {
    175         if (sInstance == null) {
    176             sInstance = new VoicemailPlaybackPresenter(activity);
    177         }
    178 
    179         sInstance.init(activity, savedInstanceState);
    180         return sInstance;
    181     }
    182 
    183     /**
    184      * Initialize variables which are activity-independent and state-independent.
    185      */
    186     private VoicemailPlaybackPresenter(Activity activity) {
    187         Context context = activity.getApplicationContext();
    188         mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor();
    189         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
    190 
    191         PowerManager powerManager =
    192                 (PowerManager) context.getSystemService(Context.POWER_SERVICE);
    193         if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
    194             mProximityWakeLock = powerManager.newWakeLock(
    195                     PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
    196         }
    197     }
    198 
    199     /**
    200      * Update variables which are activity-dependent or state-dependent.
    201      */
    202     private void init(Activity activity, Bundle savedInstanceState) {
    203         mActivity = activity;
    204         mContext = activity;
    205 
    206         mInitialOrientation = mContext.getResources().getConfiguration().orientation;
    207         mActivity.setVolumeControlStream(VoicemailPlaybackPresenter.PLAYBACK_STREAM);
    208 
    209         if (savedInstanceState != null) {
    210             // Restores playback state when activity is recreated, such as after rotation.
    211             mVoicemailUri = (Uri) savedInstanceState.getParcelable(VOICEMAIL_URI_KEY);
    212             mIsPrepared = savedInstanceState.getBoolean(IS_PREPARED_KEY);
    213             mPosition = savedInstanceState.getInt(CLIP_POSITION_KEY, 0);
    214             mIsPlaying = savedInstanceState.getBoolean(IS_PLAYING_STATE_KEY, false);
    215         }
    216 
    217         if (mMediaPlayer == null) {
    218             mIsPrepared = false;
    219             mIsPlaying = false;
    220         }
    221     }
    222 
    223     /**
    224      * Must be invoked when the parent Activity is saving it state.
    225      */
    226     public void onSaveInstanceState(Bundle outState) {
    227         if (mView != null) {
    228             outState.putParcelable(VOICEMAIL_URI_KEY, mVoicemailUri);
    229             outState.putBoolean(IS_PREPARED_KEY, mIsPrepared);
    230             outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition());
    231             outState.putBoolean(IS_PLAYING_STATE_KEY, mIsPlaying);
    232         }
    233     }
    234 
    235     /**
    236      * Specify the view which this presenter controls and the voicemail to prepare to play.
    237      */
    238     public void setPlaybackView(
    239             PlaybackView view, Uri voicemailUri, boolean startPlayingImmediately) {
    240         mView = view;
    241         mView.setPresenter(this, voicemailUri);
    242 
    243         if (mMediaPlayer != null && voicemailUri.equals(mVoicemailUri)) {
    244             // Handles case where MediaPlayer was retained after an orientation change.
    245             onPrepared(mMediaPlayer);
    246             mView.onSpeakerphoneOn(isSpeakerphoneOn());
    247         } else {
    248             if (!voicemailUri.equals(mVoicemailUri)) {
    249                 mPosition = 0;
    250             }
    251 
    252             mVoicemailUri = voicemailUri;
    253             mDuration.set(0);
    254 
    255             if (startPlayingImmediately) {
    256                 // Since setPlaybackView can get called during the view binding process, we don't
    257                 // want to reset mIsPlaying to false if the user is currently playing the
    258                 // voicemail and the view is rebound.
    259                 mIsPlaying = startPlayingImmediately;
    260                 checkForContent();
    261             }
    262 
    263             // Default to earpiece.
    264             mView.onSpeakerphoneOn(false);
    265         }
    266     }
    267 
    268     /**
    269      * Reset the presenter for playback back to its original state.
    270      */
    271     public void resetAll() {
    272         reset();
    273 
    274         mView = null;
    275         mVoicemailUri = null;
    276     }
    277 
    278     /**
    279      * Reset the presenter such that it is as if the voicemail has not been played.
    280      */
    281     public void reset() {
    282         if (mMediaPlayer != null) {
    283             mMediaPlayer.release();
    284             mMediaPlayer = null;
    285         }
    286 
    287         disableProximitySensor(false /* waitForFarState */);
    288 
    289         mIsPrepared = false;
    290         mIsPlaying = false;
    291         mPosition = 0;
    292         mDuration.set(0);
    293 
    294         if (mView != null) {
    295             mView.onPlaybackStopped();
    296             mView.setClipPosition(0, mDuration.get());
    297         }
    298     }
    299 
    300     /**
    301      * Must be invoked when the parent activity is paused.
    302      */
    303     public void onPause() {
    304         if (mContext != null && mIsPrepared
    305                 && mInitialOrientation != mContext.getResources().getConfiguration().orientation) {
    306             // If an orientation change triggers the pause, retain the MediaPlayer.
    307             Log.d(TAG, "onPause: Orientation changed.");
    308             return;
    309         }
    310 
    311         // Release the media player, otherwise there may be failures.
    312         reset();
    313 
    314         if (mActivity != null) {
    315             mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
    316         }
    317     }
    318 
    319     /**
    320      * Must be invoked when the parent activity is destroyed.
    321      */
    322     public void onDestroy() {
    323         // Clear references to avoid leaks from the singleton instance.
    324         mActivity = null;
    325         mContext = null;
    326 
    327         if (mScheduledExecutorService != null) {
    328             mScheduledExecutorService.shutdown();
    329             mScheduledExecutorService = null;
    330         }
    331 
    332         if (mFetchResultHandler != null) {
    333             mFetchResultHandler.destroy();
    334             mFetchResultHandler = null;
    335         }
    336     }
    337 
    338     /**
    339      * Checks to see if we have content available for this voicemail.
    340      * <p>
    341      * This method will be called once, after the fragment has been created, before we know if the
    342      * voicemail we've been asked to play has any content available.
    343      * <p>
    344      * Notify the user that we are fetching the content, then check to see if the content field in
    345      * the DB is set. If set, we proceed to {@link #prepareContent()} method. If not set, make
    346      * a request to fetch the content asynchronously via {@link #requestContent()}.
    347      */
    348     private void checkForContent() {
    349         mView.setIsFetchingContent();
    350         mAsyncTaskExecutor.submit(Tasks.CHECK_FOR_CONTENT, new AsyncTask<Void, Void, Boolean>() {
    351             @Override
    352             public Boolean doInBackground(Void... params) {
    353                 return queryHasContent(mVoicemailUri);
    354             }
    355 
    356             @Override
    357             public void onPostExecute(Boolean hasContent) {
    358                 if (hasContent) {
    359                     prepareContent();
    360                 } else {
    361                     requestContent();
    362                 }
    363             }
    364         });
    365     }
    366 
    367     private boolean queryHasContent(Uri voicemailUri) {
    368         if (voicemailUri == null || mContext == null) {
    369             return false;
    370         }
    371 
    372         ContentResolver contentResolver = mContext.getContentResolver();
    373         Cursor cursor = contentResolver.query(
    374                 voicemailUri, HAS_CONTENT_PROJECTION, null, null, null);
    375         try {
    376             if (cursor != null && cursor.moveToNext()) {
    377                 return cursor.getInt(cursor.getColumnIndexOrThrow(
    378                         VoicemailContract.Voicemails.HAS_CONTENT)) == 1;
    379             }
    380         } finally {
    381             MoreCloseables.closeQuietly(cursor);
    382         }
    383         return false;
    384     }
    385 
    386     /**
    387      * Makes a broadcast request to ask that a voicemail source fetch this content.
    388      * <p>
    389      * This method <b>must be called on the ui thread</b>.
    390      * <p>
    391      * This method will be called when we realise that we don't have content for this voicemail. It
    392      * will trigger a broadcast to request that the content be downloaded. It will add a listener to
    393      * the content resolver so that it will be notified when the has_content field changes. It will
    394      * also set a timer. If the has_content field changes to true within the allowed time, we will
    395      * proceed to {@link #prepareContent()}. If the has_content field does not
    396      * become true within the allowed time, we will update the ui to reflect the fact that content
    397      * was not available.
    398      */
    399     private void requestContent() {
    400         if (mFetchResultHandler != null) {
    401             mFetchResultHandler.destroy();
    402         }
    403 
    404         mFetchResultHandler = new FetchResultHandler(new Handler(), mVoicemailUri);
    405 
    406         // Send voicemail fetch request.
    407         Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, mVoicemailUri);
    408         mContext.sendBroadcast(intent);
    409     }
    410 
    411     @ThreadSafe
    412     private class FetchResultHandler extends ContentObserver implements Runnable {
    413         private AtomicBoolean mIsWaitingForResult = new AtomicBoolean(true);
    414         private final Handler mFetchResultHandler;
    415 
    416         public FetchResultHandler(Handler handler, Uri voicemailUri) {
    417             super(handler);
    418             mFetchResultHandler = handler;
    419 
    420             if (mContext != null) {
    421                 mContext.getContentResolver().registerContentObserver(
    422                         voicemailUri, false, this);
    423                 mFetchResultHandler.postDelayed(this, FETCH_CONTENT_TIMEOUT_MS);
    424             }
    425         }
    426 
    427         /**
    428          * Stop waiting for content and notify UI if {@link FETCH_CONTENT_TIMEOUT_MS} has elapsed.
    429          */
    430         @Override
    431         public void run() {
    432             if (mIsWaitingForResult.getAndSet(false) && mContext != null) {
    433                 mContext.getContentResolver().unregisterContentObserver(this);
    434                 if (mView != null) {
    435                     mView.setFetchContentTimeout();
    436                 }
    437             }
    438         }
    439 
    440         public void destroy() {
    441             if (mIsWaitingForResult.getAndSet(false) && mContext != null) {
    442                 mContext.getContentResolver().unregisterContentObserver(this);
    443                 mFetchResultHandler.removeCallbacks(this);
    444             }
    445         }
    446 
    447         @Override
    448         public void onChange(boolean selfChange) {
    449             mAsyncTaskExecutor.submit(Tasks.CHECK_CONTENT_AFTER_CHANGE,
    450                     new AsyncTask<Void, Void, Boolean>() {
    451                 @Override
    452                 public Boolean doInBackground(Void... params) {
    453                     return queryHasContent(mVoicemailUri);
    454                 }
    455 
    456                 @Override
    457                 public void onPostExecute(Boolean hasContent) {
    458                     if (hasContent && mContext != null && mIsWaitingForResult.getAndSet(false)) {
    459                         mContext.getContentResolver().unregisterContentObserver(
    460                                 FetchResultHandler.this);
    461                         prepareContent();
    462                     }
    463                 }
    464             });
    465         }
    466     }
    467 
    468     /**
    469      * Prepares the voicemail content for playback.
    470      * <p>
    471      * This method will be called once we know that our voicemail has content (according to the
    472      * content provider). this method asynchronously tries to prepare the data source through the
    473      * media player. If preparation is successful, the media player will {@link #onPrepared()},
    474      * and it will call {@link #onError()} otherwise.
    475      */
    476     private void prepareContent() {
    477         if (mView == null) {
    478             return;
    479         }
    480         Log.d(TAG, "prepareContent");
    481 
    482         // Release the previous media player, otherwise there may be failures.
    483         if (mMediaPlayer != null) {
    484             mMediaPlayer.release();
    485             mMediaPlayer = null;
    486         }
    487 
    488         mView.setIsBuffering();
    489         mIsPrepared = false;
    490 
    491         try {
    492             mMediaPlayer = new MediaPlayer();
    493             mMediaPlayer.setOnPreparedListener(this);
    494             mMediaPlayer.setOnErrorListener(this);
    495             mMediaPlayer.setOnCompletionListener(this);
    496 
    497             mMediaPlayer.reset();
    498             mMediaPlayer.setDataSource(mContext, mVoicemailUri);
    499             mMediaPlayer.setAudioStreamType(PLAYBACK_STREAM);
    500             mMediaPlayer.prepareAsync();
    501         } catch (IOException e) {
    502             handleError(e);
    503         }
    504     }
    505 
    506     /**
    507      * Once the media player is prepared, enables the UI and adopts the appropriate playback state.
    508      */
    509     @Override
    510     public void onPrepared(MediaPlayer mp) {
    511         if (mView == null) {
    512             return;
    513         }
    514         Log.d(TAG, "onPrepared");
    515         mIsPrepared = true;
    516 
    517         mDuration.set(mMediaPlayer.getDuration());
    518         mPosition = mMediaPlayer.getCurrentPosition();
    519 
    520         mView.enableUiElements();
    521         Log.d(TAG, "onPrepared: mPosition=" + mPosition);
    522         mView.setClipPosition(mPosition, mDuration.get());
    523         mMediaPlayer.seekTo(mPosition);
    524 
    525         if (mIsPlaying) {
    526             resumePlayback();
    527         } else {
    528             pausePlayback();
    529         }
    530     }
    531 
    532     /**
    533      * Invoked if preparing the media player fails, for example, if file is missing or the voicemail
    534      * is an unknown file format that can't be played.
    535      */
    536     @Override
    537     public boolean onError(MediaPlayer mp, int what, int extra) {
    538         handleError(new IllegalStateException("MediaPlayer error listener invoked: " + extra));
    539         return true;
    540     }
    541 
    542     private void handleError(Exception e) {
    543         Log.d(TAG, "handleError: Could not play voicemail " + e);
    544 
    545         if (mIsPrepared) {
    546             mMediaPlayer.release();
    547             mMediaPlayer = null;
    548             mIsPrepared = false;
    549         }
    550 
    551         if (mView != null) {
    552             mView.onPlaybackError();
    553         }
    554 
    555         mPosition = 0;
    556         mIsPlaying = false;
    557     }
    558 
    559     /**
    560      * After done playing the voicemail clip, reset the clip position to the start.
    561      */
    562     @Override
    563     public void onCompletion(MediaPlayer mediaPlayer) {
    564         pausePlayback();
    565 
    566         // Reset the seekbar position to the beginning.
    567         mPosition = 0;
    568         if (mView != null) {
    569             mView.setClipPosition(0, mDuration.get());
    570         }
    571     }
    572 
    573     @Override
    574     public void onAudioFocusChange(int focusChange) {
    575         Log.d(TAG, "onAudioFocusChange: focusChange=" + focusChange);
    576         boolean lostFocus = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT
    577                 || focusChange == AudioManager.AUDIOFOCUS_LOSS;
    578         if (mIsPlaying && focusChange == AudioManager.AUDIOFOCUS_LOSS) {
    579             pausePlayback();
    580         } else if (!mIsPlaying && focusChange == AudioManager.AUDIOFOCUS_GAIN) {
    581             resumePlayback();
    582         }
    583     }
    584 
    585     /**
    586      * Resumes voicemail playback at the clip position stored by the presenter. Null-op if already
    587      * playing.
    588      */
    589     public void resumePlayback() {
    590         if (!mIsPrepared) {
    591             // If we haven't downloaded the voicemail yet, attempt to download it.
    592             checkForContent();
    593             mIsPlaying = true;
    594 
    595             return;
    596         }
    597 
    598         mIsPlaying = true;
    599 
    600         if (!mMediaPlayer.isPlaying()) {
    601             // Clamp the start position between 0 and the duration.
    602             mPosition = Math.max(0, Math.min(mPosition, mDuration.get()));
    603             mMediaPlayer.seekTo(mPosition);
    604 
    605             try {
    606                 // Grab audio focus.
    607                 int result = mAudioManager.requestAudioFocus(
    608                         this,
    609                         PLAYBACK_STREAM,
    610                         AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
    611                 if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    612                     throw new RejectedExecutionException("Could not capture audio focus.");
    613                 }
    614 
    615                 // Can throw RejectedExecutionException.
    616                 mMediaPlayer.start();
    617             } catch (RejectedExecutionException e) {
    618                 handleError(e);
    619             }
    620         }
    621 
    622         Log.d(TAG, "Resumed playback at " + mPosition + ".");
    623         mView.onPlaybackStarted(mDuration.get(), getScheduledExecutorServiceInstance());
    624         if (isSpeakerphoneOn()) {
    625             mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
    626         } else {
    627             enableProximitySensor();
    628         }
    629     }
    630 
    631     /**
    632      * Pauses voicemail playback at the current position. Null-op if already paused.
    633      */
    634     public void pausePlayback() {
    635         if (!mIsPrepared) {
    636             return;
    637         }
    638 
    639         mIsPlaying = false;
    640 
    641         if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
    642             mMediaPlayer.pause();
    643         }
    644 
    645         mPosition = mMediaPlayer == null ? 0 : mMediaPlayer.getCurrentPosition();
    646 
    647         Log.d(TAG, "Paused playback at " + mPosition + ".");
    648 
    649         if (mView != null) {
    650             mView.onPlaybackStopped();
    651         }
    652         mAudioManager.abandonAudioFocus(this);
    653 
    654         if (mActivity != null) {
    655             mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
    656         }
    657         disableProximitySensor(true /* waitForFarState */);
    658     }
    659 
    660     /**
    661      * Pauses playback when the user starts seeking the position, and notes whether the voicemail is
    662      * playing to know whether to resume playback once the user selects a new position.
    663      */
    664     public void pausePlaybackForSeeking() {
    665         if (mMediaPlayer != null) {
    666             mShouldResumePlaybackAfterSeeking = mMediaPlayer.isPlaying();
    667         }
    668         pausePlayback();
    669     }
    670 
    671     public void resumePlaybackAfterSeeking(int desiredPosition) {
    672         mPosition = desiredPosition;
    673         if (mShouldResumePlaybackAfterSeeking) {
    674             mShouldResumePlaybackAfterSeeking = false;
    675             resumePlayback();
    676         }
    677     }
    678 
    679     private void enableProximitySensor() {
    680         if (mProximityWakeLock == null || isSpeakerphoneOn() || !mIsPrepared
    681                 || mMediaPlayer == null || !mMediaPlayer.isPlaying()) {
    682             return;
    683         }
    684 
    685         if (!mProximityWakeLock.isHeld()) {
    686             Log.i(TAG, "Acquiring proximity wake lock");
    687             mProximityWakeLock.acquire();
    688         } else {
    689             Log.i(TAG, "Proximity wake lock already acquired");
    690         }
    691     }
    692 
    693     private void disableProximitySensor(boolean waitForFarState) {
    694         if (mProximityWakeLock == null) {
    695             return;
    696         }
    697         if (mProximityWakeLock.isHeld()) {
    698             Log.i(TAG, "Releasing proximity wake lock");
    699             int flags = waitForFarState ? PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY : 0;
    700             mProximityWakeLock.release(flags);
    701         } else {
    702             Log.i(TAG, "Proximity wake lock already released");
    703         }
    704     }
    705 
    706     public void setSpeakerphoneOn(boolean on) {
    707         mAudioManager.setSpeakerphoneOn(on);
    708 
    709         if (on) {
    710             disableProximitySensor(false /* waitForFarState */);
    711             if (mIsPrepared && mMediaPlayer != null && mMediaPlayer.isPlaying()) {
    712                 mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
    713             }
    714         } else {
    715             enableProximitySensor();
    716             if (mActivity != null) {
    717                 mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
    718             }
    719         }
    720     }
    721 
    722     public boolean isSpeakerphoneOn() {
    723         return mAudioManager.isSpeakerphoneOn();
    724     }
    725 
    726     public void setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener) {
    727         mOnVoicemailDeletedListener = listener;
    728     }
    729 
    730     public int getMediaPlayerPosition() {
    731         return mIsPrepared && mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : 0;
    732     }
    733 
    734     /* package */ void onVoicemailDeleted() {
    735         // Trampoline the event notification to the interested listener
    736         if (mOnVoicemailDeletedListener != null) {
    737             mOnVoicemailDeletedListener.onVoicemailDeleted(mVoicemailUri);
    738         }
    739     }
    740 
    741     private static synchronized ScheduledExecutorService getScheduledExecutorServiceInstance() {
    742         if (mScheduledExecutorService == null) {
    743             mScheduledExecutorService = Executors.newScheduledThreadPool(NUMBER_OF_THREADS_IN_POOL);
    744         }
    745         return mScheduledExecutorService;
    746     }
    747 
    748     @VisibleForTesting
    749     public boolean isPlaying() {
    750         return mIsPlaying;
    751     }
    752 }
    753