com.android.dialer.voicemail.VoicemailPlaybackPresenter.java Source code

Java tutorial

Introduction

Here is the source code for com.android.dialer.voicemail.VoicemailPlaybackPresenter.java

Source

/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dialer.voicemail;

import com.google.common.annotations.VisibleForTesting;

import android.app.Activity;
import android.content.Context;
import android.content.ContentResolver;
import android.content.Intent;
import android.database.ContentObserver;
import android.database.Cursor;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.PowerManager;
import android.provider.VoicemailContract;
import android.support.v4.content.FileProvider;
import android.util.Log;
import android.view.WindowManager.LayoutParams;

import com.android.dialer.R;
import com.android.dialer.calllog.CallLogAsyncTaskUtil;
import com.android.dialer.util.AsyncTaskExecutor;
import com.android.dialer.util.AsyncTaskExecutors;
import com.android.common.io.MoreCloseables;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import javax.annotation.concurrent.NotThreadSafe;
import javax.annotation.concurrent.ThreadSafe;

/**
 * Contains the controlling logic for a voicemail playback in the call log. It is closely coupled
 * to assumptions about the behaviors and lifecycle of the call log, in particular in the
 * {@link CallLogFragment} and {@link CallLogAdapter}.
 * <p>
 * This controls a single {@link com.android.dialer.voicemail.VoicemailPlaybackLayout}. A single
 * instance can be reused for different such layouts, using {@link #setPlaybackView}. This
 * is to facilitate reuse across different voicemail call log entries.
 * <p>
 * This class is not thread safe. The thread policy for this class is thread-confinement, all calls
 * into this class from outside must be done from the main UI thread.
 */
@NotThreadSafe
@VisibleForTesting
public class VoicemailPlaybackPresenter
        implements MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener {

    private static final String TAG = "VmPlaybackPresenter";

    /** Contract describing the behaviour we need from the ui we are controlling. */
    public interface PlaybackView {
        int getDesiredClipPosition();

        void disableUiElements();

        void enableUiElements();

        void onPlaybackError();

        void onPlaybackStarted(int duration, ScheduledExecutorService executorService);

        void onPlaybackStopped();

        void onSpeakerphoneOn(boolean on);

        void setClipPosition(int clipPositionInMillis, int clipLengthInMillis);

        void setSuccess();

        void setFetchContentTimeout();

        void setIsFetchingContent();

        void onVoicemailArchiveSucceded(Uri voicemailUri);

        void onVoicemailArchiveFailed(Uri voicemailUri);

        void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri);

        void resetSeekBar();
    }

    public interface OnVoicemailDeletedListener {
        void onVoicemailDeleted(Uri uri);

        void onVoicemailDeleteUndo();

        void onVoicemailDeletedInDatabase();
    }

    /** The enumeration of {@link AsyncTask} objects we use in this class. */
    public enum Tasks {
        CHECK_FOR_CONTENT, CHECK_CONTENT_AFTER_CHANGE, ARCHIVE_VOICEMAIL
    }

    protected interface OnContentCheckedListener {
        void onContentChecked(boolean hasContent);
    }

    private static final String[] HAS_CONTENT_PROJECTION = new String[] { VoicemailContract.Voicemails.HAS_CONTENT,
            VoicemailContract.Voicemails.DURATION };

    private static final int NUMBER_OF_THREADS_IN_POOL = 2;
    // Time to wait for content to be fetched before timing out.
    private static final long FETCH_CONTENT_TIMEOUT_MS = 20000;

    private static final String VOICEMAIL_URI_KEY = VoicemailPlaybackPresenter.class.getName() + ".VOICEMAIL_URI";
    private static final String IS_PREPARED_KEY = VoicemailPlaybackPresenter.class.getName() + ".IS_PREPARED";
    // If present in the saved instance bundle, we should not resume playback on create.
    private static final String IS_PLAYING_STATE_KEY = VoicemailPlaybackPresenter.class.getName()
            + ".IS_PLAYING_STATE_KEY";
    // If present in the saved instance bundle, indicates where to set the playback slider.
    private static final String CLIP_POSITION_KEY = VoicemailPlaybackPresenter.class.getName()
            + ".CLIP_POSITION_KEY";
    private static final String IS_SPEAKERPHONE_ON_KEY = VoicemailPlaybackPresenter.class.getName()
            + ".IS_SPEAKER_PHONE_ON";
    public static final int PLAYBACK_REQUEST = 0;
    public static final int ARCHIVE_REQUEST = 1;
    public static final int SHARE_REQUEST = 2;

    /**
     * The most recently cached duration. We cache this since we don't want to keep requesting it
     * from the player, as this can easily lead to throwing {@link IllegalStateException} (any time
     * the player is released, it's illegal to ask for the duration).
     */
    private final AtomicInteger mDuration = new AtomicInteger(0);

    private static VoicemailPlaybackPresenter sInstance;

    private Activity mActivity;
    protected Context mContext;
    private PlaybackView mView;
    protected Uri mVoicemailUri;

    protected MediaPlayer mMediaPlayer;
    private int mPosition;
    private boolean mIsPlaying;
    // MediaPlayer crashes on some method calls if not prepared but does not have a method which
    // exposes its prepared state. Store this locally, so we can check and prevent crashes.
    private boolean mIsPrepared;
    private boolean mIsSpeakerphoneOn;

    private boolean mShouldResumePlaybackAfterSeeking;
    private int mInitialOrientation;

    // Used to run async tasks that need to interact with the UI.
    protected AsyncTaskExecutor mAsyncTaskExecutor;
    private static ScheduledExecutorService mScheduledExecutorService;
    /**
     * Used to handle the result of a successful or time-out fetch result.
     * <p>
     * This variable is thread-contained, accessed only on the ui thread.
     */
    private FetchResultHandler mFetchResultHandler;
    private final List<FetchResultHandler> mArchiveResultHandlers = new ArrayList<>();
    private Handler mHandler = new Handler();
    private PowerManager.WakeLock mProximityWakeLock;
    private VoicemailAudioManager mVoicemailAudioManager;

    private OnVoicemailDeletedListener mOnVoicemailDeletedListener;
    private final VoicemailAsyncTaskUtil mVoicemailAsyncTaskUtil;

    /**
     * Obtain singleton instance of this class. Use a single instance to provide a consistent
     * listener to the AudioManager when requesting and abandoning audio focus.
     *
     * Otherwise, after rotation the previous listener will still be active but a new listener
     * will be provided to calls to the AudioManager, which is bad. For example, abandoning
     * audio focus with the new listeners results in an AUDIO_FOCUS_GAIN callback to the
     * previous listener, which is the opposite of the intended behavior.
     */
    public static VoicemailPlaybackPresenter getInstance(Activity activity, Bundle savedInstanceState) {
        if (sInstance == null) {
            sInstance = new VoicemailPlaybackPresenter(activity);
        }

        sInstance.init(activity, savedInstanceState);
        return sInstance;
    }

    /**
     * Initialize variables which are activity-independent and state-independent.
     */
    protected VoicemailPlaybackPresenter(Activity activity) {
        Context context = activity.getApplicationContext();
        mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor();
        mVoicemailAudioManager = new VoicemailAudioManager(context, this);
        mVoicemailAsyncTaskUtil = new VoicemailAsyncTaskUtil(context.getContentResolver());
        PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
        if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
            mProximityWakeLock = powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
        }
    }

    /**
     * Update variables which are activity-dependent or state-dependent.
     */
    protected void init(Activity activity, Bundle savedInstanceState) {
        mActivity = activity;
        mContext = activity;

        mInitialOrientation = mContext.getResources().getConfiguration().orientation;
        mActivity.setVolumeControlStream(VoicemailAudioManager.PLAYBACK_STREAM);

        if (savedInstanceState != null) {
            // Restores playback state when activity is recreated, such as after rotation.
            mVoicemailUri = (Uri) savedInstanceState.getParcelable(VOICEMAIL_URI_KEY);
            mIsPrepared = savedInstanceState.getBoolean(IS_PREPARED_KEY);
            mPosition = savedInstanceState.getInt(CLIP_POSITION_KEY, 0);
            mIsPlaying = savedInstanceState.getBoolean(IS_PLAYING_STATE_KEY, false);
            mIsSpeakerphoneOn = savedInstanceState.getBoolean(IS_SPEAKERPHONE_ON_KEY, false);
        }

        if (mMediaPlayer == null) {
            mIsPrepared = false;
            mIsPlaying = false;
        }
    }

    /**
     * Must be invoked when the parent Activity is saving it state.
     */
    public void onSaveInstanceState(Bundle outState) {
        if (mView != null) {
            outState.putParcelable(VOICEMAIL_URI_KEY, mVoicemailUri);
            outState.putBoolean(IS_PREPARED_KEY, mIsPrepared);
            outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition());
            outState.putBoolean(IS_PLAYING_STATE_KEY, mIsPlaying);
            outState.putBoolean(IS_SPEAKERPHONE_ON_KEY, mIsSpeakerphoneOn);
        }
    }

    /**
     * Specify the view which this presenter controls and the voicemail to prepare to play.
     */
    public void setPlaybackView(PlaybackView view, Uri voicemailUri, boolean startPlayingImmediately) {
        mView = view;
        mView.setPresenter(this, voicemailUri);

        // Handles cases where the same entry is binded again when scrolling in list, or where
        // the MediaPlayer was retained after an orientation change.
        if (mMediaPlayer != null && mIsPrepared && voicemailUri.equals(mVoicemailUri)) {
            // If the voicemail card was rebinded, we need to set the position to the appropriate
            // point. Since we retain the media player, we can just set it to the position of the
            // media player.
            mPosition = mMediaPlayer.getCurrentPosition();
            onPrepared(mMediaPlayer);
        } else {
            if (!voicemailUri.equals(mVoicemailUri)) {
                mVoicemailUri = voicemailUri;
                mPosition = 0;
                // Default to earpiece.
                setSpeakerphoneOn(false);
                mVoicemailAudioManager.setSpeakerphoneOn(false);
            } else {
                // Update the view to the current speakerphone state.
                mView.onSpeakerphoneOn(mIsSpeakerphoneOn);
            }
            /*
             * Check to see if the content field in the DB is set. If set, we proceed to
             * prepareContent() method. We get the duration of the voicemail from the query and set
             * it if the content is not available.
             */
            checkForContent(new OnContentCheckedListener() {
                @Override
                public void onContentChecked(boolean hasContent) {
                    if (hasContent) {
                        prepareContent();
                    } else if (mView != null) {
                        mView.resetSeekBar();
                        mView.setClipPosition(0, mDuration.get());
                    }
                }
            });

            if (startPlayingImmediately) {
                // Since setPlaybackView can get called during the view binding process, we don't
                // want to reset mIsPlaying to false if the user is currently playing the
                // voicemail and the view is rebound.
                mIsPlaying = startPlayingImmediately;
            }
        }
    }

    /**
     * Reset the presenter for playback back to its original state.
     */
    public void resetAll() {
        pausePresenter(true);

        mView = null;
        mVoicemailUri = null;
    }

    /**
     * When navigating away from voicemail playback, we need to release the media player,
     * pause the UI and save the position.
     *
     * @param reset {@code true} if we want to reset the position of the playback, {@code false} if
     * we want to retain the current position (in case we return to the voicemail).
     */
    public void pausePresenter(boolean reset) {
        if (mMediaPlayer != null) {
            mMediaPlayer.release();
            mMediaPlayer = null;
        }

        disableProximitySensor(false /* waitForFarState */);

        mIsPrepared = false;
        mIsPlaying = false;

        if (reset) {
            // We want to reset the position whether or not the view is valid.
            mPosition = 0;
        }

        if (mView != null) {
            mView.onPlaybackStopped();
            if (reset) {
                mView.setClipPosition(0, mDuration.get());
            } else {
                mPosition = mView.getDesiredClipPosition();
            }
        }
    }

    /**
     * Must be invoked when the parent activity is resumed.
     */
    public void onResume() {
        mVoicemailAudioManager.registerReceivers();
    }

    /**
     * Must be invoked when the parent activity is paused.
     */
    public void onPause() {
        mVoicemailAudioManager.unregisterReceivers();

        if (mContext != null && mIsPrepared
                && mInitialOrientation != mContext.getResources().getConfiguration().orientation) {
            // If an orientation change triggers the pause, retain the MediaPlayer.
            Log.d(TAG, "onPause: Orientation changed.");
            return;
        }

        // Release the media player, otherwise there may be failures.
        pausePresenter(false);

        if (mActivity != null) {
            mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
        }

    }

    /**
     * Must be invoked when the parent activity is destroyed.
     */
    public void onDestroy() {
        // Clear references to avoid leaks from the singleton instance.
        mActivity = null;
        mContext = null;

        if (mScheduledExecutorService != null) {
            mScheduledExecutorService.shutdown();
            mScheduledExecutorService = null;
        }

        if (!mArchiveResultHandlers.isEmpty()) {
            for (FetchResultHandler fetchResultHandler : mArchiveResultHandlers) {
                fetchResultHandler.destroy();
            }
            mArchiveResultHandlers.clear();
        }

        if (mFetchResultHandler != null) {
            mFetchResultHandler.destroy();
            mFetchResultHandler = null;
        }
    }

    /**
     * Checks to see if we have content available for this voicemail.
     */
    protected void checkForContent(final OnContentCheckedListener callback) {
        mAsyncTaskExecutor.submit(Tasks.CHECK_FOR_CONTENT, new AsyncTask<Void, Void, Boolean>() {
            @Override
            public Boolean doInBackground(Void... params) {
                return queryHasContent(mVoicemailUri);
            }

            @Override
            public void onPostExecute(Boolean hasContent) {
                callback.onContentChecked(hasContent);
            }
        });
    }

    private boolean queryHasContent(Uri voicemailUri) {
        if (voicemailUri == null || mContext == null) {
            return false;
        }

        ContentResolver contentResolver = mContext.getContentResolver();
        Cursor cursor = contentResolver.query(voicemailUri, null, null, null, null);
        try {
            if (cursor != null && cursor.moveToNext()) {
                int duration = cursor.getInt(cursor.getColumnIndex(VoicemailContract.Voicemails.DURATION));
                // Convert database duration (seconds) into mDuration (milliseconds)
                mDuration.set(duration > 0 ? duration * 1000 : 0);
                return cursor.getInt(cursor.getColumnIndex(VoicemailContract.Voicemails.HAS_CONTENT)) == 1;
            }
        } finally {
            MoreCloseables.closeQuietly(cursor);
        }
        return false;
    }

    /**
     * Makes a broadcast request to ask that a voicemail source fetch this content.
     * <p>
     * This method <b>must be called on the ui thread</b>.
     * <p>
     * This method will be called when we realise that we don't have content for this voicemail. It
     * will trigger a broadcast to request that the content be downloaded. It will add a listener to
     * the content resolver so that it will be notified when the has_content field changes. It will
     * also set a timer. If the has_content field changes to true within the allowed time, we will
     * proceed to {@link #prepareContent()}. If the has_content field does not
     * become true within the allowed time, we will update the ui to reflect the fact that content
     * was not available.
     *
     * @return whether issued request to fetch content
     */
    protected boolean requestContent(int code) {
        if (mContext == null || mVoicemailUri == null) {
            return false;
        }

        FetchResultHandler tempFetchResultHandler = new FetchResultHandler(new Handler(), mVoicemailUri, code);

        switch (code) {
        case ARCHIVE_REQUEST:
            mArchiveResultHandlers.add(tempFetchResultHandler);
            break;
        default:
            if (mFetchResultHandler != null) {
                mFetchResultHandler.destroy();
            }
            mView.setIsFetchingContent();
            mFetchResultHandler = tempFetchResultHandler;
            break;
        }

        // Send voicemail fetch request.
        Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, mVoicemailUri);
        mContext.sendBroadcast(intent);
        return true;
    }

    @ThreadSafe
    private class FetchResultHandler extends ContentObserver implements Runnable {
        private AtomicBoolean mIsWaitingForResult = new AtomicBoolean(true);
        private final Handler mFetchResultHandler;
        private final Uri mVoicemailUri;
        private final int mRequestCode;

        public FetchResultHandler(Handler handler, Uri uri, int code) {
            super(handler);
            mFetchResultHandler = handler;
            mRequestCode = code;
            mVoicemailUri = uri;
            if (mContext != null) {
                mContext.getContentResolver().registerContentObserver(mVoicemailUri, false, this);
                mFetchResultHandler.postDelayed(this, FETCH_CONTENT_TIMEOUT_MS);
            }
        }

        /**
         * Stop waiting for content and notify UI if {@link FETCH_CONTENT_TIMEOUT_MS} has elapsed.
         */
        @Override
        public void run() {
            if (mIsWaitingForResult.getAndSet(false) && mContext != null) {
                mContext.getContentResolver().unregisterContentObserver(this);
                if (mView != null) {
                    mView.setFetchContentTimeout();
                }
            }
        }

        public void destroy() {
            if (mIsWaitingForResult.getAndSet(false) && mContext != null) {
                mContext.getContentResolver().unregisterContentObserver(this);
                mFetchResultHandler.removeCallbacks(this);
            }
        }

        @Override
        public void onChange(boolean selfChange) {
            mAsyncTaskExecutor.submit(Tasks.CHECK_CONTENT_AFTER_CHANGE, new AsyncTask<Void, Void, Boolean>() {

                @Override
                public Boolean doInBackground(Void... params) {
                    return queryHasContent(mVoicemailUri);
                }

                @Override
                public void onPostExecute(Boolean hasContent) {
                    if (hasContent && mContext != null && mIsWaitingForResult.getAndSet(false)) {
                        mContext.getContentResolver().unregisterContentObserver(FetchResultHandler.this);
                        prepareContent();
                        if (mRequestCode == ARCHIVE_REQUEST) {
                            startArchiveVoicemailTask(mVoicemailUri, true /* archivedByUser */);
                        } else if (mRequestCode == SHARE_REQUEST) {
                            startArchiveVoicemailTask(mVoicemailUri, false /* archivedByUser */);
                        }
                    }
                }
            });
        }
    }

    /**
     * Prepares the voicemail content for playback.
     * <p>
     * This method will be called once we know that our voicemail has content (according to the
     * content provider). this method asynchronously tries to prepare the data source through the
     * media player. If preparation is successful, the media player will {@link #onPrepared()},
     * and it will call {@link #onError()} otherwise.
     */
    protected void prepareContent() {
        if (mView == null) {
            return;
        }
        Log.d(TAG, "prepareContent");

        // Release the previous media player, otherwise there may be failures.
        if (mMediaPlayer != null) {
            mMediaPlayer.release();
            mMediaPlayer = null;
        }

        mView.disableUiElements();
        mIsPrepared = false;

        try {
            mMediaPlayer = new MediaPlayer();
            mMediaPlayer.setOnPreparedListener(this);
            mMediaPlayer.setOnErrorListener(this);
            mMediaPlayer.setOnCompletionListener(this);

            mMediaPlayer.reset();
            mMediaPlayer.setDataSource(mContext, mVoicemailUri);
            mMediaPlayer.setAudioStreamType(VoicemailAudioManager.PLAYBACK_STREAM);
            mMediaPlayer.prepareAsync();
        } catch (IOException e) {
            handleError(e);
        }
    }

    /**
     * Once the media player is prepared, enables the UI and adopts the appropriate playback state.
     */
    @Override
    public void onPrepared(MediaPlayer mp) {
        if (mView == null) {
            return;
        }
        Log.d(TAG, "onPrepared");
        mIsPrepared = true;

        // Update the duration in the database if it was not previously retrieved
        CallLogAsyncTaskUtil.updateVoicemailDuration(mContext, mVoicemailUri,
                TimeUnit.MILLISECONDS.toSeconds(mMediaPlayer.getDuration()));

        mDuration.set(mMediaPlayer.getDuration());

        Log.d(TAG, "onPrepared: mPosition=" + mPosition);
        mView.setClipPosition(mPosition, mDuration.get());
        mView.enableUiElements();
        mView.setSuccess();
        mMediaPlayer.seekTo(mPosition);

        if (mIsPlaying) {
            resumePlayback();
        } else {
            pausePlayback();
        }
    }

    /**
     * Invoked if preparing the media player fails, for example, if file is missing or the voicemail
     * is an unknown file format that can't be played.
     */
    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        handleError(new IllegalStateException("MediaPlayer error listener invoked: " + extra));
        return true;
    }

    protected void handleError(Exception e) {
        Log.d(TAG, "handleError: Could not play voicemail " + e);

        if (mIsPrepared) {
            mMediaPlayer.release();
            mMediaPlayer = null;
            mIsPrepared = false;
        }

        if (mView != null) {
            mView.onPlaybackError();
        }

        mPosition = 0;
        mIsPlaying = false;
    }

    /**
     * After done playing the voicemail clip, reset the clip position to the start.
     */
    @Override
    public void onCompletion(MediaPlayer mediaPlayer) {
        pausePlayback();

        // Reset the seekbar position to the beginning.
        mPosition = 0;
        if (mView != null) {
            mView.setClipPosition(0, mDuration.get());
        }
    }

    /**
     * Only play voicemail when audio focus is granted. When it is lost (usually by another
     * application requesting focus), pause playback.
     *
     * @param gainedFocus {@code true} if the audio focus was gained, {@code} false otherwise.
     */
    public void onAudioFocusChange(boolean gainedFocus) {
        if (mIsPlaying == gainedFocus) {
            // Nothing new here, just exit.
            return;
        }

        if (!mIsPlaying) {
            resumePlayback();
        } else {
            pausePlayback();
        }
    }

    /**
     * Resumes voicemail playback at the clip position stored by the presenter. Null-op if already
     * playing.
     */
    public void resumePlayback() {
        if (mView == null) {
            return;
        }

        if (!mIsPrepared) {
            /*
             * Check content before requesting content to avoid duplicated requests. It is possible
             * that the UI doesn't know content has arrived if the fetch took too long causing a
             * timeout, but succeeded.
             */
            checkForContent(new OnContentCheckedListener() {
                @Override
                public void onContentChecked(boolean hasContent) {
                    if (!hasContent) {
                        // No local content, download from server. Queue playing if the request was
                        // issued,
                        mIsPlaying = requestContent(PLAYBACK_REQUEST);
                    } else {
                        // Queue playing once the media play loaded the content.
                        mIsPlaying = true;
                        prepareContent();
                    }
                }
            });
            return;
        }

        mIsPlaying = true;

        if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) {
            // Clamp the start position between 0 and the duration.
            mPosition = Math.max(0, Math.min(mPosition, mDuration.get()));

            mMediaPlayer.seekTo(mPosition);

            try {
                // Grab audio focus.
                // Can throw RejectedExecutionException.
                mVoicemailAudioManager.requestAudioFocus();
                mMediaPlayer.start();
                setSpeakerphoneOn(mIsSpeakerphoneOn);
            } catch (RejectedExecutionException e) {
                handleError(e);
            }
        }

        Log.d(TAG, "Resumed playback at " + mPosition + ".");
        mView.onPlaybackStarted(mDuration.get(), getScheduledExecutorServiceInstance());
    }

    /**
     * Pauses voicemail playback at the current position. Null-op if already paused.
     */
    public void pausePlayback() {
        if (!mIsPrepared) {
            return;
        }

        mIsPlaying = false;

        if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
            mMediaPlayer.pause();
        }

        mPosition = mMediaPlayer == null ? 0 : mMediaPlayer.getCurrentPosition();

        Log.d(TAG, "Paused playback at " + mPosition + ".");

        if (mView != null) {
            mView.onPlaybackStopped();
        }

        mVoicemailAudioManager.abandonAudioFocus();

        if (mActivity != null) {
            mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
        }
        disableProximitySensor(true /* waitForFarState */);
    }

    /**
     * Pauses playback when the user starts seeking the position, and notes whether the voicemail is
     * playing to know whether to resume playback once the user selects a new position.
     */
    public void pausePlaybackForSeeking() {
        if (mMediaPlayer != null) {
            mShouldResumePlaybackAfterSeeking = mMediaPlayer.isPlaying();
        }
        pausePlayback();
    }

    public void resumePlaybackAfterSeeking(int desiredPosition) {
        mPosition = desiredPosition;
        if (mShouldResumePlaybackAfterSeeking) {
            mShouldResumePlaybackAfterSeeking = false;
            resumePlayback();
        }
    }

    /**
     * Seek to position. This is called when user manually seek the playback. It could be either
     * by touch or volume button while in talkback mode.
     * @param position
     */
    public void seek(int position) {
        mPosition = position;
    }

    private void enableProximitySensor() {
        if (mProximityWakeLock == null || mIsSpeakerphoneOn || !mIsPrepared || mMediaPlayer == null
                || !mMediaPlayer.isPlaying()) {
            return;
        }

        if (!mProximityWakeLock.isHeld()) {
            Log.i(TAG, "Acquiring proximity wake lock");
            mProximityWakeLock.acquire();
        } else {
            Log.i(TAG, "Proximity wake lock already acquired");
        }
    }

    private void disableProximitySensor(boolean waitForFarState) {
        if (mProximityWakeLock == null) {
            return;
        }
        if (mProximityWakeLock.isHeld()) {
            Log.i(TAG, "Releasing proximity wake lock");
            int flags = waitForFarState ? PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY : 0;
            mProximityWakeLock.release(flags);
        } else {
            Log.i(TAG, "Proximity wake lock already released");
        }
    }

    /**
     * This is for use by UI interactions only. It simplifies UI logic.
     */
    public void toggleSpeakerphone() {
        mVoicemailAudioManager.setSpeakerphoneOn(!mIsSpeakerphoneOn);
        setSpeakerphoneOn(!mIsSpeakerphoneOn);
    }

    /**
     * This method only handles app-level changes to the speakerphone. Audio layer changes should
     * be handled separately. This is so that the VoicemailAudioManager can trigger changes to
     * the presenter without the presenter triggering the audio manager and duplicating actions.
     */
    public void setSpeakerphoneOn(boolean on) {
        if (mView == null) {
            return;
        }

        mView.onSpeakerphoneOn(on);

        mIsSpeakerphoneOn = on;

        // This should run even if speakerphone is not being toggled because we may be switching
        // from earpiece to headphone and vise versa. Also upon initial setup the default audio
        // source is the earpiece, so we want to trigger the proximity sensor.
        if (mIsPlaying) {
            if (on || mVoicemailAudioManager.isWiredHeadsetPluggedIn()) {
                disableProximitySensor(false /* waitForFarState */);
                if (mIsPrepared && mMediaPlayer != null && mMediaPlayer.isPlaying()) {
                    mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
                }
            } else {
                enableProximitySensor();
                if (mActivity != null) {
                    mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
                }
            }
        }
    }

    public void setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener) {
        mOnVoicemailDeletedListener = listener;
    }

    public int getMediaPlayerPosition() {
        return mIsPrepared && mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : 0;
    }

    public void notifyUiOfArchiveResult(Uri voicemailUri, boolean archived) {
        if (mView == null) {
            return;
        }
        if (archived) {
            mView.onVoicemailArchiveSucceded(voicemailUri);
        } else {
            mView.onVoicemailArchiveFailed(voicemailUri);
        }
    }

    /* package */ void onVoicemailDeleted() {
        // Trampoline the event notification to the interested listener.
        if (mOnVoicemailDeletedListener != null) {
            mOnVoicemailDeletedListener.onVoicemailDeleted(mVoicemailUri);
        }
    }

    /* package */ void onVoicemailDeleteUndo() {
        // Trampoline the event notification to the interested listener.
        if (mOnVoicemailDeletedListener != null) {
            mOnVoicemailDeletedListener.onVoicemailDeleteUndo();
        }
    }

    /* package */ void onVoicemailDeletedInDatabase() {
        // Trampoline the event notification to the interested listener.
        if (mOnVoicemailDeletedListener != null) {
            mOnVoicemailDeletedListener.onVoicemailDeletedInDatabase();
        }
    }

    private static synchronized ScheduledExecutorService getScheduledExecutorServiceInstance() {
        if (mScheduledExecutorService == null) {
            mScheduledExecutorService = Executors.newScheduledThreadPool(NUMBER_OF_THREADS_IN_POOL);
        }
        return mScheduledExecutorService;
    }

    /**
     * If voicemail has already been downloaded, go straight to archiving. Otherwise, request
     * the voicemail content first.
     */
    public void archiveContent(final Uri voicemailUri, final boolean archivedByUser) {
        checkForContent(new OnContentCheckedListener() {
            @Override
            public void onContentChecked(boolean hasContent) {
                if (!hasContent) {
                    requestContent(archivedByUser ? ARCHIVE_REQUEST : SHARE_REQUEST);
                } else {
                    startArchiveVoicemailTask(voicemailUri, archivedByUser);
                }
            }
        });
    }

    /**
     * Asynchronous task used to archive a voicemail given its uri.
     */
    protected void startArchiveVoicemailTask(final Uri voicemailUri, final boolean archivedByUser) {
        mVoicemailAsyncTaskUtil.archiveVoicemailContent(new VoicemailAsyncTaskUtil.OnArchiveVoicemailListener() {
            @Override
            public void onArchiveVoicemail(final Uri archivedVoicemailUri) {
                if (archivedVoicemailUri == null) {
                    notifyUiOfArchiveResult(voicemailUri, false);
                    return;
                }

                if (archivedByUser) {
                    setArchivedVoicemailStatusAndUpdateUI(voicemailUri, archivedVoicemailUri, true);
                } else {
                    sendShareIntent(archivedVoicemailUri);
                }
            }
        }, voicemailUri);
    }

    /**
     * Sends the intent for sharing the voicemail file.
     */
    protected void sendShareIntent(final Uri voicemailUri) {
        mVoicemailAsyncTaskUtil
                .getVoicemailFilePath(new VoicemailAsyncTaskUtil.OnGetArchivedVoicemailFilePathListener() {
                    @Override
                    public void onGetArchivedVoicemailFilePath(String filePath) {
                        mView.enableUiElements();
                        if (filePath == null) {
                            mView.setFetchContentTimeout();
                            return;
                        }
                        Uri voicemailFileUri = FileProvider.getUriForFile(mContext,
                                mContext.getString(R.string.contacts_file_provider_authority), new File(filePath));
                        mContext.startActivity(Intent.createChooser(getShareIntent(voicemailFileUri),
                                mContext.getResources().getText(R.string.call_log_share_voicemail)));
                    }
                }, voicemailUri);
    }

    /** Sets archived_by_user field to the given boolean and updates the URI. */
    private void setArchivedVoicemailStatusAndUpdateUI(final Uri voicemailUri, final Uri archivedVoicemailUri,
            boolean status) {
        mVoicemailAsyncTaskUtil
                .setVoicemailArchiveStatus(new VoicemailAsyncTaskUtil.OnSetVoicemailArchiveStatusListener() {
                    @Override
                    public void onSetVoicemailArchiveStatus(boolean success) {
                        notifyUiOfArchiveResult(voicemailUri, success);
                    }
                }, archivedVoicemailUri, status);
    }

    private Intent getShareIntent(Uri voicemailFileUri) {
        Intent shareIntent = new Intent();
        shareIntent.setAction(Intent.ACTION_SEND);
        shareIntent.putExtra(Intent.EXTRA_STREAM, voicemailFileUri);
        shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        shareIntent.setType(mContext.getContentResolver().getType(voicemailFileUri));
        return shareIntent;
    }

    @VisibleForTesting
    public boolean isPlaying() {
        return mIsPlaying;
    }

    @VisibleForTesting
    public boolean isSpeakerphoneOn() {
        return mIsSpeakerphoneOn;
    }

    @VisibleForTesting
    public void clearInstance() {
        sInstance = null;
    }
}