• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.dialer.voicemail;
18 
19 import 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 {
getDesiredClipPosition()81         int getDesiredClipPosition();
disableUiElements()82         void disableUiElements();
enableUiElements()83         void enableUiElements();
onPlaybackError()84         void onPlaybackError();
onPlaybackStarted(int duration, ScheduledExecutorService executorService)85         void onPlaybackStarted(int duration, ScheduledExecutorService executorService);
onPlaybackStopped()86         void onPlaybackStopped();
onSpeakerphoneOn(boolean on)87         void onSpeakerphoneOn(boolean on);
setClipPosition(int clipPositionInMillis, int clipLengthInMillis)88         void setClipPosition(int clipPositionInMillis, int clipLengthInMillis);
setFetchContentTimeout()89         void setFetchContentTimeout();
setIsBuffering()90         void setIsBuffering();
setIsFetchingContent()91         void setIsFetchingContent();
setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri)92         void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri);
93     }
94 
95     public interface OnVoicemailDeletedListener {
onVoicemailDeleted(Uri uri)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      */
getInstance( Activity activity, Bundle savedInstanceState)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      */
VoicemailPlaybackPresenter(Activity activity)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      */
init(Activity activity, Bundle savedInstanceState)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      */
onSaveInstanceState(Bundle outState)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      */
setPlaybackView( PlaybackView view, Uri voicemailUri, boolean startPlayingImmediately)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      */
resetAll()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      */
reset()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      */
onPause()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      */
onDestroy()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      */
checkForContent()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 
queryHasContent(Uri voicemailUri)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      */
requestContent()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 
FetchResultHandler(Handler handler, Uri voicemailUri)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
run()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 
destroy()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
onChange(boolean selfChange)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      */
prepareContent()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
onPrepared(MediaPlayer mp)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
onError(MediaPlayer mp, int what, int extra)537     public boolean onError(MediaPlayer mp, int what, int extra) {
538         handleError(new IllegalStateException("MediaPlayer error listener invoked: " + extra));
539         return true;
540     }
541 
handleError(Exception e)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
onCompletion(MediaPlayer mediaPlayer)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
onAudioFocusChange(int focusChange)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      */
resumePlayback()589     public void resumePlayback() {
590         if (mView == null || mContext == null) {
591             return;
592         }
593 
594         if (!mIsPrepared) {
595             // If we haven't downloaded the voicemail yet, attempt to download it.
596             checkForContent();
597             mIsPlaying = true;
598 
599             return;
600         }
601 
602         mIsPlaying = true;
603 
604         if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) {
605             // Clamp the start position between 0 and the duration.
606             mPosition = Math.max(0, Math.min(mPosition, mDuration.get()));
607             mMediaPlayer.seekTo(mPosition);
608 
609             try {
610                 // Grab audio focus.
611                 int result = mAudioManager.requestAudioFocus(
612                         this,
613                         PLAYBACK_STREAM,
614                         AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
615                 if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
616                     throw new RejectedExecutionException("Could not capture audio focus.");
617                 }
618 
619                 // Can throw RejectedExecutionException.
620                 mMediaPlayer.start();
621             } catch (RejectedExecutionException e) {
622                 handleError(e);
623             }
624         }
625 
626         Log.d(TAG, "Resumed playback at " + mPosition + ".");
627         mView.onPlaybackStarted(mDuration.get(), getScheduledExecutorServiceInstance());
628         if (isSpeakerphoneOn()) {
629             mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
630         } else {
631             enableProximitySensor();
632         }
633     }
634 
635     /**
636      * Pauses voicemail playback at the current position. Null-op if already paused.
637      */
pausePlayback()638     public void pausePlayback() {
639         if (!mIsPrepared) {
640             return;
641         }
642 
643         mIsPlaying = false;
644 
645         if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
646             mMediaPlayer.pause();
647         }
648 
649         mPosition = mMediaPlayer == null ? 0 : mMediaPlayer.getCurrentPosition();
650 
651         Log.d(TAG, "Paused playback at " + mPosition + ".");
652 
653         if (mView != null) {
654             mView.onPlaybackStopped();
655         }
656         mAudioManager.abandonAudioFocus(this);
657 
658         if (mActivity != null) {
659             mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
660         }
661         disableProximitySensor(true /* waitForFarState */);
662     }
663 
664     /**
665      * Pauses playback when the user starts seeking the position, and notes whether the voicemail is
666      * playing to know whether to resume playback once the user selects a new position.
667      */
pausePlaybackForSeeking()668     public void pausePlaybackForSeeking() {
669         if (mMediaPlayer != null) {
670             mShouldResumePlaybackAfterSeeking = mMediaPlayer.isPlaying();
671         }
672         pausePlayback();
673     }
674 
resumePlaybackAfterSeeking(int desiredPosition)675     public void resumePlaybackAfterSeeking(int desiredPosition) {
676         mPosition = desiredPosition;
677         if (mShouldResumePlaybackAfterSeeking) {
678             mShouldResumePlaybackAfterSeeking = false;
679             resumePlayback();
680         }
681     }
682 
enableProximitySensor()683     private void enableProximitySensor() {
684         if (mProximityWakeLock == null || isSpeakerphoneOn() || !mIsPrepared
685                 || mMediaPlayer == null || !mMediaPlayer.isPlaying()) {
686             return;
687         }
688 
689         if (!mProximityWakeLock.isHeld()) {
690             Log.i(TAG, "Acquiring proximity wake lock");
691             mProximityWakeLock.acquire();
692         } else {
693             Log.i(TAG, "Proximity wake lock already acquired");
694         }
695     }
696 
disableProximitySensor(boolean waitForFarState)697     private void disableProximitySensor(boolean waitForFarState) {
698         if (mProximityWakeLock == null) {
699             return;
700         }
701         if (mProximityWakeLock.isHeld()) {
702             Log.i(TAG, "Releasing proximity wake lock");
703             int flags = waitForFarState ? PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY : 0;
704             mProximityWakeLock.release(flags);
705         } else {
706             Log.i(TAG, "Proximity wake lock already released");
707         }
708     }
709 
setSpeakerphoneOn(boolean on)710     public void setSpeakerphoneOn(boolean on) {
711         mAudioManager.setSpeakerphoneOn(on);
712 
713         if (on) {
714             disableProximitySensor(false /* waitForFarState */);
715             if (mIsPrepared && mMediaPlayer != null && mMediaPlayer.isPlaying()) {
716                 mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
717             }
718         } else {
719             enableProximitySensor();
720             if (mActivity != null) {
721                 mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
722             }
723         }
724     }
725 
isSpeakerphoneOn()726     public boolean isSpeakerphoneOn() {
727         return mAudioManager.isSpeakerphoneOn();
728     }
729 
setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener)730     public void setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener) {
731         mOnVoicemailDeletedListener = listener;
732     }
733 
getMediaPlayerPosition()734     public int getMediaPlayerPosition() {
735         return mIsPrepared && mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : 0;
736     }
737 
onVoicemailDeleted()738     /* package */ void onVoicemailDeleted() {
739         // Trampoline the event notification to the interested listener
740         if (mOnVoicemailDeletedListener != null) {
741             mOnVoicemailDeletedListener.onVoicemailDeleted(mVoicemailUri);
742         }
743     }
744 
getScheduledExecutorServiceInstance()745     private static synchronized ScheduledExecutorService getScheduledExecutorServiceInstance() {
746         if (mScheduledExecutorService == null) {
747             mScheduledExecutorService = Executors.newScheduledThreadPool(NUMBER_OF_THREADS_IN_POOL);
748         }
749         return mScheduledExecutorService;
750     }
751 
752     @VisibleForTesting
isPlaying()753     public boolean isPlaying() {
754         return mIsPlaying;
755     }
756 }
757