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 com.google.common.annotations.VisibleForTesting;
20
21import android.app.Activity;
22import android.content.Context;
23import android.content.ContentResolver;
24import android.content.Intent;
25import android.database.ContentObserver;
26import android.database.Cursor;
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.support.v4.content.FileProvider;
35import android.util.Log;
36import android.view.WindowManager.LayoutParams;
37
38import com.android.dialer.R;
39import com.android.dialer.calllog.CallLogAsyncTaskUtil;
40import com.android.dialer.util.AsyncTaskExecutor;
41import com.android.dialer.util.AsyncTaskExecutors;
42import com.android.common.io.MoreCloseables;
43
44import java.io.File;
45import java.io.IOException;
46import java.util.ArrayList;
47import java.util.List;
48import java.util.concurrent.Executors;
49import java.util.concurrent.RejectedExecutionException;
50import java.util.concurrent.ScheduledExecutorService;
51import java.util.concurrent.TimeUnit;
52import java.util.concurrent.atomic.AtomicBoolean;
53import java.util.concurrent.atomic.AtomicInteger;
54
55import javax.annotation.concurrent.NotThreadSafe;
56import javax.annotation.concurrent.ThreadSafe;
57
58/**
59 * Contains the controlling logic for a voicemail playback in the call log. It is closely coupled
60 * to assumptions about the behaviors and lifecycle of the call log, in particular in the
61 * {@link CallLogFragment} and {@link CallLogAdapter}.
62 * <p>
63 * This controls a single {@link com.android.dialer.voicemail.VoicemailPlaybackLayout}. A single
64 * instance can be reused for different such layouts, using {@link #setPlaybackView}. This
65 * is to facilitate reuse across different voicemail call log entries.
66 * <p>
67 * This class is not thread safe. The thread policy for this class is thread-confinement, all calls
68 * into this class from outside must be done from the main UI thread.
69 */
70@NotThreadSafe
71@VisibleForTesting
72public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListener,
73                MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener {
74
75    private static final String TAG = "VmPlaybackPresenter";
76
77    /** Contract describing the behaviour we need from the ui we are controlling. */
78    public interface PlaybackView {
79        int getDesiredClipPosition();
80        void disableUiElements();
81        void enableUiElements();
82        void onPlaybackError();
83        void onPlaybackStarted(int duration, ScheduledExecutorService executorService);
84        void onPlaybackStopped();
85        void onSpeakerphoneOn(boolean on);
86        void setClipPosition(int clipPositionInMillis, int clipLengthInMillis);
87        void setSuccess();
88        void setFetchContentTimeout();
89        void setIsFetchingContent();
90        void onVoicemailArchiveSucceded(Uri voicemailUri);
91        void onVoicemailArchiveFailed(Uri voicemailUri);
92        void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri);
93        void resetSeekBar();
94    }
95
96    public interface OnVoicemailDeletedListener {
97        void onVoicemailDeleted(Uri uri);
98        void onVoicemailDeleteUndo();
99        void onVoicemailDeletedInDatabase();
100    }
101
102    /** The enumeration of {@link AsyncTask} objects we use in this class. */
103    public enum Tasks {
104        CHECK_FOR_CONTENT,
105        CHECK_CONTENT_AFTER_CHANGE,
106        ARCHIVE_VOICEMAIL
107    }
108
109    protected interface OnContentCheckedListener {
110        void onContentChecked(boolean hasContent);
111    }
112
113    private static final String[] HAS_CONTENT_PROJECTION = new String[] {
114        VoicemailContract.Voicemails.HAS_CONTENT,
115        VoicemailContract.Voicemails.DURATION
116    };
117
118    private static final int NUMBER_OF_THREADS_IN_POOL = 2;
119    // Time to wait for content to be fetched before timing out.
120    private static final long FETCH_CONTENT_TIMEOUT_MS = 20000;
121
122    private static final String VOICEMAIL_URI_KEY =
123            VoicemailPlaybackPresenter.class.getName() + ".VOICEMAIL_URI";
124    private static final String IS_PREPARED_KEY =
125            VoicemailPlaybackPresenter.class.getName() + ".IS_PREPARED";
126    // If present in the saved instance bundle, we should not resume playback on create.
127    private static final String IS_PLAYING_STATE_KEY =
128            VoicemailPlaybackPresenter.class.getName() + ".IS_PLAYING_STATE_KEY";
129    // If present in the saved instance bundle, indicates where to set the playback slider.
130    private static final String CLIP_POSITION_KEY =
131            VoicemailPlaybackPresenter.class.getName() + ".CLIP_POSITION_KEY";
132    private static final String IS_SPEAKERPHONE_ON_KEY =
133            VoicemailPlaybackPresenter.class.getName() + ".IS_SPEAKER_PHONE_ON";
134    public static final int PLAYBACK_REQUEST = 0;
135    public static final int ARCHIVE_REQUEST = 1;
136    public static final int SHARE_REQUEST = 2;
137
138    /**
139     * The most recently cached duration. We cache this since we don't want to keep requesting it
140     * from the player, as this can easily lead to throwing {@link IllegalStateException} (any time
141     * the player is released, it's illegal to ask for the duration).
142     */
143    private final AtomicInteger mDuration = new AtomicInteger(0);
144
145    private static VoicemailPlaybackPresenter sInstance;
146
147    private Activity mActivity;
148    protected Context mContext;
149    private PlaybackView mView;
150    protected Uri mVoicemailUri;
151
152    protected MediaPlayer mMediaPlayer;
153    private int mPosition;
154    private boolean mIsPlaying;
155    // MediaPlayer crashes on some method calls if not prepared but does not have a method which
156    // exposes its prepared state. Store this locally, so we can check and prevent crashes.
157    private boolean mIsPrepared;
158    private boolean mIsSpeakerphoneOn;
159
160    private boolean mShouldResumePlaybackAfterSeeking;
161    private int mInitialOrientation;
162
163    // Used to run async tasks that need to interact with the UI.
164    protected AsyncTaskExecutor mAsyncTaskExecutor;
165    private static ScheduledExecutorService mScheduledExecutorService;
166    /**
167     * Used to handle the result of a successful or time-out fetch result.
168     * <p>
169     * This variable is thread-contained, accessed only on the ui thread.
170     */
171    private FetchResultHandler mFetchResultHandler;
172    private final List<FetchResultHandler> mArchiveResultHandlers = new ArrayList<>();
173    private Handler mHandler = new Handler();
174    private PowerManager.WakeLock mProximityWakeLock;
175    private VoicemailAudioManager mVoicemailAudioManager;
176
177    private OnVoicemailDeletedListener mOnVoicemailDeletedListener;
178    private final VoicemailAsyncTaskUtil mVoicemailAsyncTaskUtil;
179
180    /**
181     * Obtain singleton instance of this class. Use a single instance to provide a consistent
182     * listener to the AudioManager when requesting and abandoning audio focus.
183     *
184     * Otherwise, after rotation the previous listener will still be active but a new listener
185     * will be provided to calls to the AudioManager, which is bad. For example, abandoning
186     * audio focus with the new listeners results in an AUDIO_FOCUS_GAIN callback to the
187     * previous listener, which is the opposite of the intended behavior.
188     */
189    public static VoicemailPlaybackPresenter getInstance(
190            Activity activity, Bundle savedInstanceState) {
191        if (sInstance == null) {
192            sInstance = new VoicemailPlaybackPresenter(activity);
193        }
194
195        sInstance.init(activity, savedInstanceState);
196        return sInstance;
197    }
198
199    /**
200     * Initialize variables which are activity-independent and state-independent.
201     */
202    protected VoicemailPlaybackPresenter(Activity activity) {
203        Context context = activity.getApplicationContext();
204        mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor();
205        mVoicemailAudioManager = new VoicemailAudioManager(context, this);
206        mVoicemailAsyncTaskUtil = new VoicemailAsyncTaskUtil(context.getContentResolver());
207        PowerManager powerManager =
208                (PowerManager) context.getSystemService(Context.POWER_SERVICE);
209        if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
210            mProximityWakeLock = powerManager.newWakeLock(
211                    PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
212        }
213    }
214
215    /**
216     * Update variables which are activity-dependent or state-dependent.
217     */
218    protected void init(Activity activity, Bundle savedInstanceState) {
219        mActivity = activity;
220        mContext = activity;
221
222        mInitialOrientation = mContext.getResources().getConfiguration().orientation;
223        mActivity.setVolumeControlStream(VoicemailAudioManager.PLAYBACK_STREAM);
224
225        if (savedInstanceState != null) {
226            // Restores playback state when activity is recreated, such as after rotation.
227            mVoicemailUri = (Uri) savedInstanceState.getParcelable(VOICEMAIL_URI_KEY);
228            mIsPrepared = savedInstanceState.getBoolean(IS_PREPARED_KEY);
229            mPosition = savedInstanceState.getInt(CLIP_POSITION_KEY, 0);
230            mIsPlaying = savedInstanceState.getBoolean(IS_PLAYING_STATE_KEY, false);
231            mIsSpeakerphoneOn = savedInstanceState.getBoolean(IS_SPEAKERPHONE_ON_KEY, false);
232        }
233
234        if (mMediaPlayer == null) {
235            mIsPrepared = false;
236            mIsPlaying = false;
237        }
238    }
239
240    /**
241     * Must be invoked when the parent Activity is saving it state.
242     */
243    public void onSaveInstanceState(Bundle outState) {
244        if (mView != null) {
245            outState.putParcelable(VOICEMAIL_URI_KEY, mVoicemailUri);
246            outState.putBoolean(IS_PREPARED_KEY, mIsPrepared);
247            outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition());
248            outState.putBoolean(IS_PLAYING_STATE_KEY, mIsPlaying);
249            outState.putBoolean(IS_SPEAKERPHONE_ON_KEY, mIsSpeakerphoneOn);
250        }
251    }
252
253    /**
254     * Specify the view which this presenter controls and the voicemail to prepare to play.
255     */
256    public void setPlaybackView(
257            PlaybackView view, Uri voicemailUri, boolean startPlayingImmediately) {
258        mView = view;
259        mView.setPresenter(this, voicemailUri);
260
261        // Handles cases where the same entry is binded again when scrolling in list, or where
262        // the MediaPlayer was retained after an orientation change.
263        if (mMediaPlayer != null && mIsPrepared && voicemailUri.equals(mVoicemailUri)) {
264            // If the voicemail card was rebinded, we need to set the position to the appropriate
265            // point. Since we retain the media player, we can just set it to the position of the
266            // media player.
267            mPosition = mMediaPlayer.getCurrentPosition();
268            onPrepared(mMediaPlayer);
269        } else {
270            if (!voicemailUri.equals(mVoicemailUri)) {
271                mVoicemailUri = voicemailUri;
272                mPosition = 0;
273                // Default to earpiece.
274                setSpeakerphoneOn(false);
275                mVoicemailAudioManager.setSpeakerphoneOn(false);
276            } else {
277                // Update the view to the current speakerphone state.
278                mView.onSpeakerphoneOn(mIsSpeakerphoneOn);
279            }
280            /*
281             * Check to see if the content field in the DB is set. If set, we proceed to
282             * prepareContent() method. We get the duration of the voicemail from the query and set
283             * it if the content is not available.
284             */
285            checkForContent(new OnContentCheckedListener() {
286                @Override
287                public void onContentChecked(boolean hasContent) {
288                    if (hasContent) {
289                        prepareContent();
290                    } else if (mView != null) {
291                        mView.resetSeekBar();
292                        mView.setClipPosition(0, mDuration.get());
293                    }
294                }
295            });
296
297            if (startPlayingImmediately) {
298                // Since setPlaybackView can get called during the view binding process, we don't
299                // want to reset mIsPlaying to false if the user is currently playing the
300                // voicemail and the view is rebound.
301                mIsPlaying = startPlayingImmediately;
302            }
303        }
304    }
305
306    /**
307     * Reset the presenter for playback back to its original state.
308     */
309    public void resetAll() {
310        pausePresenter(true);
311
312        mView = null;
313        mVoicemailUri = null;
314    }
315
316    /**
317     * When navigating away from voicemail playback, we need to release the media player,
318     * pause the UI and save the position.
319     *
320     * @param reset {@code true} if we want to reset the position of the playback, {@code false} if
321     * we want to retain the current position (in case we return to the voicemail).
322     */
323    public void pausePresenter(boolean reset) {
324        if (mMediaPlayer != null) {
325            mMediaPlayer.release();
326            mMediaPlayer = null;
327        }
328
329        disableProximitySensor(false /* waitForFarState */);
330
331        mIsPrepared = false;
332        mIsPlaying = false;
333
334        if (reset) {
335            // We want to reset the position whether or not the view is valid.
336            mPosition = 0;
337        }
338
339        if (mView != null) {
340            mView.onPlaybackStopped();
341            if (reset) {
342                mView.setClipPosition(0, mDuration.get());
343            } else {
344                mPosition = mView.getDesiredClipPosition();
345            }
346        }
347    }
348
349    /**
350     * Must be invoked when the parent activity is resumed.
351     */
352    public void onResume() {
353        mVoicemailAudioManager.registerReceivers();
354    }
355
356    /**
357     * Must be invoked when the parent activity is paused.
358     */
359    public void onPause() {
360        mVoicemailAudioManager.unregisterReceivers();
361
362        if (mContext != null && mIsPrepared
363                && mInitialOrientation != mContext.getResources().getConfiguration().orientation) {
364            // If an orientation change triggers the pause, retain the MediaPlayer.
365            Log.d(TAG, "onPause: Orientation changed.");
366            return;
367        }
368
369        // Release the media player, otherwise there may be failures.
370        pausePresenter(false);
371
372        if (mActivity != null) {
373            mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
374        }
375
376    }
377
378    /**
379     * Must be invoked when the parent activity is destroyed.
380     */
381    public void onDestroy() {
382        // Clear references to avoid leaks from the singleton instance.
383        mActivity = null;
384        mContext = null;
385
386        if (mScheduledExecutorService != null) {
387            mScheduledExecutorService.shutdown();
388            mScheduledExecutorService = null;
389        }
390
391        if (!mArchiveResultHandlers.isEmpty()) {
392            for (FetchResultHandler fetchResultHandler : mArchiveResultHandlers) {
393                fetchResultHandler.destroy();
394            }
395            mArchiveResultHandlers.clear();
396        }
397
398        if (mFetchResultHandler != null) {
399            mFetchResultHandler.destroy();
400            mFetchResultHandler = null;
401        }
402    }
403
404    /**
405     * Checks to see if we have content available for this voicemail.
406     */
407    protected void checkForContent(final OnContentCheckedListener callback) {
408        mAsyncTaskExecutor.submit(Tasks.CHECK_FOR_CONTENT, new AsyncTask<Void, Void, Boolean>() {
409            @Override
410            public Boolean doInBackground(Void... params) {
411                return queryHasContent(mVoicemailUri);
412            }
413
414            @Override
415            public void onPostExecute(Boolean hasContent) {
416                callback.onContentChecked(hasContent);
417            }
418        });
419    }
420
421    private boolean queryHasContent(Uri voicemailUri) {
422        if (voicemailUri == null || mContext == null) {
423            return false;
424        }
425
426        ContentResolver contentResolver = mContext.getContentResolver();
427        Cursor cursor = contentResolver.query(
428                voicemailUri, null, null, null, null);
429        try {
430            if (cursor != null && cursor.moveToNext()) {
431                int duration = cursor.getInt(cursor.getColumnIndex(
432                        VoicemailContract.Voicemails.DURATION));
433                // Convert database duration (seconds) into mDuration (milliseconds)
434                mDuration.set(duration > 0 ? duration * 1000 : 0);
435                return cursor.getInt(cursor.getColumnIndex(
436                        VoicemailContract.Voicemails.HAS_CONTENT)) == 1;
437            }
438        } finally {
439            MoreCloseables.closeQuietly(cursor);
440        }
441        return false;
442    }
443
444    /**
445     * Makes a broadcast request to ask that a voicemail source fetch this content.
446     * <p>
447     * This method <b>must be called on the ui thread</b>.
448     * <p>
449     * This method will be called when we realise that we don't have content for this voicemail. It
450     * will trigger a broadcast to request that the content be downloaded. It will add a listener to
451     * the content resolver so that it will be notified when the has_content field changes. It will
452     * also set a timer. If the has_content field changes to true within the allowed time, we will
453     * proceed to {@link #prepareContent()}. If the has_content field does not
454     * become true within the allowed time, we will update the ui to reflect the fact that content
455     * was not available.
456     *
457     * @return whether issued request to fetch content
458     */
459    protected boolean requestContent(int code) {
460        if (mContext == null || mVoicemailUri == null) {
461            return false;
462        }
463
464        FetchResultHandler tempFetchResultHandler =
465                new FetchResultHandler(new Handler(), mVoicemailUri, code);
466
467        switch (code) {
468            case ARCHIVE_REQUEST:
469                mArchiveResultHandlers.add(tempFetchResultHandler);
470                break;
471            default:
472                if (mFetchResultHandler != null) {
473                    mFetchResultHandler.destroy();
474                }
475                mView.setIsFetchingContent();
476                mFetchResultHandler = tempFetchResultHandler;
477                break;
478        }
479
480        // Send voicemail fetch request.
481        Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, mVoicemailUri);
482        mContext.sendBroadcast(intent);
483        return true;
484    }
485
486    @ThreadSafe
487    private class FetchResultHandler extends ContentObserver implements Runnable {
488        private AtomicBoolean mIsWaitingForResult = new AtomicBoolean(true);
489        private final Handler mFetchResultHandler;
490        private final Uri mVoicemailUri;
491        private final int mRequestCode;
492
493        public FetchResultHandler(Handler handler, Uri uri, int code) {
494            super(handler);
495            mFetchResultHandler = handler;
496            mRequestCode = code;
497            mVoicemailUri = uri;
498            if (mContext != null) {
499                mContext.getContentResolver().registerContentObserver(
500                        mVoicemailUri, false, this);
501                mFetchResultHandler.postDelayed(this, FETCH_CONTENT_TIMEOUT_MS);
502            }
503        }
504
505        /**
506         * Stop waiting for content and notify UI if {@link FETCH_CONTENT_TIMEOUT_MS} has elapsed.
507         */
508        @Override
509        public void run() {
510            if (mIsWaitingForResult.getAndSet(false) && mContext != null) {
511                mContext.getContentResolver().unregisterContentObserver(this);
512                if (mView != null) {
513                    mView.setFetchContentTimeout();
514                }
515            }
516        }
517
518        public void destroy() {
519            if (mIsWaitingForResult.getAndSet(false) && mContext != null) {
520                mContext.getContentResolver().unregisterContentObserver(this);
521                mFetchResultHandler.removeCallbacks(this);
522            }
523        }
524
525        @Override
526        public void onChange(boolean selfChange) {
527            mAsyncTaskExecutor.submit(Tasks.CHECK_CONTENT_AFTER_CHANGE,
528                    new AsyncTask<Void, Void, Boolean>() {
529
530                @Override
531                public Boolean doInBackground(Void... params) {
532                    return queryHasContent(mVoicemailUri);
533                }
534
535                @Override
536                public void onPostExecute(Boolean hasContent) {
537                    if (hasContent && mContext != null && mIsWaitingForResult.getAndSet(false)) {
538                        mContext.getContentResolver().unregisterContentObserver(
539                                FetchResultHandler.this);
540                        prepareContent();
541                        if (mRequestCode == ARCHIVE_REQUEST) {
542                            startArchiveVoicemailTask(mVoicemailUri, true /* archivedByUser */);
543                        } else if (mRequestCode == SHARE_REQUEST) {
544                            startArchiveVoicemailTask(mVoicemailUri, false /* archivedByUser */);
545                        }
546                    }
547                }
548            });
549        }
550    }
551
552    /**
553     * Prepares the voicemail content for playback.
554     * <p>
555     * This method will be called once we know that our voicemail has content (according to the
556     * content provider). this method asynchronously tries to prepare the data source through the
557     * media player. If preparation is successful, the media player will {@link #onPrepared()},
558     * and it will call {@link #onError()} otherwise.
559     */
560    protected void prepareContent() {
561        if (mView == null) {
562            return;
563        }
564        Log.d(TAG, "prepareContent");
565
566        // Release the previous media player, otherwise there may be failures.
567        if (mMediaPlayer != null) {
568            mMediaPlayer.release();
569            mMediaPlayer = null;
570        }
571
572        mView.disableUiElements();
573        mIsPrepared = false;
574
575        try {
576            mMediaPlayer = new MediaPlayer();
577            mMediaPlayer.setOnPreparedListener(this);
578            mMediaPlayer.setOnErrorListener(this);
579            mMediaPlayer.setOnCompletionListener(this);
580
581            mMediaPlayer.reset();
582            mMediaPlayer.setDataSource(mContext, mVoicemailUri);
583            mMediaPlayer.setAudioStreamType(VoicemailAudioManager.PLAYBACK_STREAM);
584            mMediaPlayer.prepareAsync();
585        } catch (IOException e) {
586            handleError(e);
587        }
588    }
589
590    /**
591     * Once the media player is prepared, enables the UI and adopts the appropriate playback state.
592     */
593    @Override
594    public void onPrepared(MediaPlayer mp) {
595        if (mView == null) {
596            return;
597        }
598        Log.d(TAG, "onPrepared");
599        mIsPrepared = true;
600
601        // Update the duration in the database if it was not previously retrieved
602        CallLogAsyncTaskUtil.updateVoicemailDuration(mContext, mVoicemailUri,
603                TimeUnit.MILLISECONDS.toSeconds(mMediaPlayer.getDuration()));
604
605        mDuration.set(mMediaPlayer.getDuration());
606
607        Log.d(TAG, "onPrepared: mPosition=" + mPosition);
608        mView.setClipPosition(mPosition, mDuration.get());
609        mView.enableUiElements();
610        mView.setSuccess();
611        mMediaPlayer.seekTo(mPosition);
612
613        if (mIsPlaying) {
614            resumePlayback();
615        } else {
616            pausePlayback();
617        }
618    }
619
620    /**
621     * Invoked if preparing the media player fails, for example, if file is missing or the voicemail
622     * is an unknown file format that can't be played.
623     */
624    @Override
625    public boolean onError(MediaPlayer mp, int what, int extra) {
626        handleError(new IllegalStateException("MediaPlayer error listener invoked: " + extra));
627        return true;
628    }
629
630    protected void handleError(Exception e) {
631        Log.d(TAG, "handleError: Could not play voicemail " + e);
632
633        if (mIsPrepared) {
634            mMediaPlayer.release();
635            mMediaPlayer = null;
636            mIsPrepared = false;
637        }
638
639        if (mView != null) {
640            mView.onPlaybackError();
641        }
642
643        mPosition = 0;
644        mIsPlaying = false;
645    }
646
647    /**
648     * After done playing the voicemail clip, reset the clip position to the start.
649     */
650    @Override
651    public void onCompletion(MediaPlayer mediaPlayer) {
652        pausePlayback();
653
654        // Reset the seekbar position to the beginning.
655        mPosition = 0;
656        if (mView != null) {
657            mView.setClipPosition(0, mDuration.get());
658        }
659    }
660
661    /**
662     * Only play voicemail when audio focus is granted. When it is lost (usually by another
663     * application requesting focus), pause playback.
664     *
665     * @param gainedFocus {@code true} if the audio focus was gained, {@code} false otherwise.
666     */
667    public void onAudioFocusChange(boolean gainedFocus) {
668        if (mIsPlaying == gainedFocus) {
669            // Nothing new here, just exit.
670            return;
671        }
672
673        if (!mIsPlaying) {
674            resumePlayback();
675        } else {
676            pausePlayback();
677        }
678    }
679
680    /**
681     * Resumes voicemail playback at the clip position stored by the presenter. Null-op if already
682     * playing.
683     */
684    public void resumePlayback() {
685        if (mView == null) {
686            return;
687        }
688
689        if (!mIsPrepared) {
690            /*
691             * Check content before requesting content to avoid duplicated requests. It is possible
692             * that the UI doesn't know content has arrived if the fetch took too long causing a
693             * timeout, but succeeded.
694             */
695            checkForContent(new OnContentCheckedListener() {
696                @Override
697                public void onContentChecked(boolean hasContent) {
698                    if (!hasContent) {
699                        // No local content, download from server. Queue playing if the request was
700                        // issued,
701                        mIsPlaying = requestContent(PLAYBACK_REQUEST);
702                    } else {
703                        // Queue playing once the media play loaded the content.
704                        mIsPlaying = true;
705                        prepareContent();
706                    }
707                }
708            });
709            return;
710        }
711
712        mIsPlaying = true;
713
714        if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) {
715            // Clamp the start position between 0 and the duration.
716            mPosition = Math.max(0, Math.min(mPosition, mDuration.get()));
717
718            mMediaPlayer.seekTo(mPosition);
719
720            try {
721                // Grab audio focus.
722                // Can throw RejectedExecutionException.
723                mVoicemailAudioManager.requestAudioFocus();
724                mMediaPlayer.start();
725                setSpeakerphoneOn(mIsSpeakerphoneOn);
726            } catch (RejectedExecutionException e) {
727                handleError(e);
728            }
729        }
730
731        Log.d(TAG, "Resumed playback at " + mPosition + ".");
732        mView.onPlaybackStarted(mDuration.get(), getScheduledExecutorServiceInstance());
733    }
734
735    /**
736     * Pauses voicemail playback at the current position. Null-op if already paused.
737     */
738    public void pausePlayback() {
739        if (!mIsPrepared) {
740            return;
741        }
742
743        mIsPlaying = false;
744
745        if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
746            mMediaPlayer.pause();
747        }
748
749        mPosition = mMediaPlayer == null ? 0 : mMediaPlayer.getCurrentPosition();
750
751        Log.d(TAG, "Paused playback at " + mPosition + ".");
752
753        if (mView != null) {
754            mView.onPlaybackStopped();
755        }
756
757        mVoicemailAudioManager.abandonAudioFocus();
758
759        if (mActivity != null) {
760            mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
761        }
762        disableProximitySensor(true /* waitForFarState */);
763    }
764
765    /**
766     * Pauses playback when the user starts seeking the position, and notes whether the voicemail is
767     * playing to know whether to resume playback once the user selects a new position.
768     */
769    public void pausePlaybackForSeeking() {
770        if (mMediaPlayer != null) {
771            mShouldResumePlaybackAfterSeeking = mMediaPlayer.isPlaying();
772        }
773        pausePlayback();
774    }
775
776    public void resumePlaybackAfterSeeking(int desiredPosition) {
777        mPosition = desiredPosition;
778        if (mShouldResumePlaybackAfterSeeking) {
779            mShouldResumePlaybackAfterSeeking = false;
780            resumePlayback();
781        }
782    }
783
784    /**
785     * Seek to position. This is called when user manually seek the playback. It could be either
786     * by touch or volume button while in talkback mode.
787     * @param position
788     */
789    public void seek(int position) {
790        mPosition = position;
791    }
792
793    private void enableProximitySensor() {
794        if (mProximityWakeLock == null || mIsSpeakerphoneOn || !mIsPrepared
795                || mMediaPlayer == null || !mMediaPlayer.isPlaying()) {
796            return;
797        }
798
799        if (!mProximityWakeLock.isHeld()) {
800            Log.i(TAG, "Acquiring proximity wake lock");
801            mProximityWakeLock.acquire();
802        } else {
803            Log.i(TAG, "Proximity wake lock already acquired");
804        }
805    }
806
807    private void disableProximitySensor(boolean waitForFarState) {
808        if (mProximityWakeLock == null) {
809            return;
810        }
811        if (mProximityWakeLock.isHeld()) {
812            Log.i(TAG, "Releasing proximity wake lock");
813            int flags = waitForFarState ? PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY : 0;
814            mProximityWakeLock.release(flags);
815        } else {
816            Log.i(TAG, "Proximity wake lock already released");
817        }
818    }
819
820    /**
821     * This is for use by UI interactions only. It simplifies UI logic.
822     */
823    public void toggleSpeakerphone() {
824        mVoicemailAudioManager.setSpeakerphoneOn(!mIsSpeakerphoneOn);
825        setSpeakerphoneOn(!mIsSpeakerphoneOn);
826    }
827
828    /**
829     * This method only handles app-level changes to the speakerphone. Audio layer changes should
830     * be handled separately. This is so that the VoicemailAudioManager can trigger changes to
831     * the presenter without the presenter triggering the audio manager and duplicating actions.
832     */
833    public void setSpeakerphoneOn(boolean on) {
834        if (mView == null) {
835            return;
836        }
837
838        mView.onSpeakerphoneOn(on);
839
840        mIsSpeakerphoneOn = on;
841
842        // This should run even if speakerphone is not being toggled because we may be switching
843        // from earpiece to headphone and vise versa. Also upon initial setup the default audio
844        // source is the earpiece, so we want to trigger the proximity sensor.
845        if (mIsPlaying) {
846            if (on || mVoicemailAudioManager.isWiredHeadsetPluggedIn()) {
847                disableProximitySensor(false /* waitForFarState */);
848                if (mIsPrepared && mMediaPlayer != null && mMediaPlayer.isPlaying()) {
849                    mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
850                }
851            } else {
852                enableProximitySensor();
853                if (mActivity != null) {
854                    mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
855                }
856            }
857        }
858    }
859
860    public void setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener) {
861        mOnVoicemailDeletedListener = listener;
862    }
863
864    public int getMediaPlayerPosition() {
865        return mIsPrepared && mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : 0;
866    }
867
868    public void notifyUiOfArchiveResult(Uri voicemailUri, boolean archived) {
869        if (mView == null) {
870            return;
871        }
872        if (archived) {
873            mView.onVoicemailArchiveSucceded(voicemailUri);
874        } else {
875            mView.onVoicemailArchiveFailed(voicemailUri);
876        }
877    }
878
879    /* package */ void onVoicemailDeleted() {
880        // Trampoline the event notification to the interested listener.
881        if (mOnVoicemailDeletedListener != null) {
882            mOnVoicemailDeletedListener.onVoicemailDeleted(mVoicemailUri);
883        }
884    }
885
886    /* package */ void onVoicemailDeleteUndo() {
887        // Trampoline the event notification to the interested listener.
888        if (mOnVoicemailDeletedListener != null) {
889            mOnVoicemailDeletedListener.onVoicemailDeleteUndo();
890        }
891    }
892
893    /* package */ void onVoicemailDeletedInDatabase() {
894        // Trampoline the event notification to the interested listener.
895        if (mOnVoicemailDeletedListener != null) {
896            mOnVoicemailDeletedListener.onVoicemailDeletedInDatabase();
897        }
898    }
899
900    private static synchronized ScheduledExecutorService getScheduledExecutorServiceInstance() {
901        if (mScheduledExecutorService == null) {
902            mScheduledExecutorService = Executors.newScheduledThreadPool(NUMBER_OF_THREADS_IN_POOL);
903        }
904        return mScheduledExecutorService;
905    }
906
907    /**
908     * If voicemail has already been downloaded, go straight to archiving. Otherwise, request
909     * the voicemail content first.
910     */
911    public void archiveContent(final Uri voicemailUri, final boolean archivedByUser) {
912        checkForContent(new OnContentCheckedListener() {
913            @Override
914            public void onContentChecked(boolean hasContent) {
915                if (!hasContent) {
916                    requestContent(archivedByUser ? ARCHIVE_REQUEST : SHARE_REQUEST);
917                } else {
918                    startArchiveVoicemailTask(voicemailUri, archivedByUser);
919                }
920            }
921        });
922    }
923
924    /**
925     * Asynchronous task used to archive a voicemail given its uri.
926     */
927    protected void startArchiveVoicemailTask(final Uri voicemailUri, final boolean archivedByUser) {
928        mVoicemailAsyncTaskUtil.archiveVoicemailContent(
929                new VoicemailAsyncTaskUtil.OnArchiveVoicemailListener() {
930                    @Override
931                    public void onArchiveVoicemail(final Uri archivedVoicemailUri) {
932                        if (archivedVoicemailUri == null) {
933                            notifyUiOfArchiveResult(voicemailUri, false);
934                            return;
935                        }
936
937                        if (archivedByUser) {
938                            setArchivedVoicemailStatusAndUpdateUI(voicemailUri,
939                                    archivedVoicemailUri, true);
940                        } else {
941                            sendShareIntent(archivedVoicemailUri);
942                        }
943                    }
944                }, voicemailUri);
945    }
946
947    /**
948     * Sends the intent for sharing the voicemail file.
949     */
950    protected void sendShareIntent(final Uri voicemailUri) {
951        mVoicemailAsyncTaskUtil.getVoicemailFilePath(
952                new VoicemailAsyncTaskUtil.OnGetArchivedVoicemailFilePathListener() {
953                    @Override
954                    public void onGetArchivedVoicemailFilePath(String filePath) {
955                        mView.enableUiElements();
956                        if (filePath == null) {
957                            mView.setFetchContentTimeout();
958                            return;
959                        }
960                        Uri voicemailFileUri = FileProvider.getUriForFile(
961                                mContext,
962                                mContext.getString(R.string.contacts_file_provider_authority),
963                                new File(filePath));
964                        mContext.startActivity(Intent.createChooser(
965                                getShareIntent(voicemailFileUri),
966                                mContext.getResources().getText(
967                                        R.string.call_log_share_voicemail)));
968                    }
969                }, voicemailUri);
970    }
971
972    /** Sets archived_by_user field to the given boolean and updates the URI. */
973    private void setArchivedVoicemailStatusAndUpdateUI(
974            final Uri voicemailUri,
975            final Uri archivedVoicemailUri,
976            boolean status) {
977        mVoicemailAsyncTaskUtil.setVoicemailArchiveStatus(
978                new VoicemailAsyncTaskUtil.OnSetVoicemailArchiveStatusListener() {
979                    @Override
980                    public void onSetVoicemailArchiveStatus(boolean success) {
981                        notifyUiOfArchiveResult(voicemailUri, success);
982                    }
983                }, archivedVoicemailUri, status);
984    }
985
986    private Intent getShareIntent(Uri voicemailFileUri) {
987        Intent shareIntent = new Intent();
988        shareIntent.setAction(Intent.ACTION_SEND);
989        shareIntent.putExtra(Intent.EXTRA_STREAM, voicemailFileUri);
990        shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
991        shareIntent.setType(mContext.getContentResolver()
992                .getType(voicemailFileUri));
993        return shareIntent;
994    }
995
996    @VisibleForTesting
997    public boolean isPlaying() {
998        return mIsPlaying;
999    }
1000
1001    @VisibleForTesting
1002    public boolean isSpeakerphoneOn() {
1003        return mIsSpeakerphoneOn;
1004    }
1005
1006    @VisibleForTesting
1007    public void clearInstance() {
1008        sInstance = null;
1009    }
1010}
1011