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
17package com.android.dialer.voicemail;
18
19import android.app.Activity;
20import android.content.Context;
21import android.content.ContentResolver;
22import android.content.Intent;
23import android.database.ContentObserver;
24import android.database.Cursor;
25import android.media.AudioManager;
26import android.media.AudioManager.OnAudioFocusChangeListener;
27import android.media.MediaPlayer;
28import android.net.Uri;
29import android.os.AsyncTask;
30import android.os.Bundle;
31import android.os.Handler;
32import android.os.PowerManager;
33import android.provider.VoicemailContract;
34import android.util.Log;
35import android.view.View;
36import android.view.WindowManager.LayoutParams;
37import android.widget.SeekBar;
38
39import com.android.dialer.R;
40import com.android.dialer.util.AsyncTaskExecutor;
41import com.android.dialer.util.AsyncTaskExecutors;
42
43import com.android.common.io.MoreCloseables;
44import com.google.common.annotations.VisibleForTesting;
45import com.google.common.base.Preconditions;
46
47import java.io.IOException;
48import java.util.concurrent.Executors;
49import java.util.concurrent.ScheduledExecutorService;
50import java.util.concurrent.RejectedExecutionException;
51import java.util.concurrent.ScheduledExecutorService;
52import java.util.concurrent.ScheduledFuture;
53import java.util.concurrent.atomic.AtomicBoolean;
54import java.util.concurrent.atomic.AtomicInteger;
55
56import javax.annotation.concurrent.NotThreadSafe;
57import 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
73public 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