Home | History | Annotate | Download | only in voicemail
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.dialer.voicemail;
     18 
     19 import com.google.common.annotations.VisibleForTesting;
     20 
     21 import android.app.Activity;
     22 import android.content.Context;
     23 import android.content.ContentResolver;
     24 import android.content.Intent;
     25 import android.database.ContentObserver;
     26 import android.database.Cursor;
     27 import android.media.MediaPlayer;
     28 import android.net.Uri;
     29 import android.os.AsyncTask;
     30 import android.os.Bundle;
     31 import android.os.Handler;
     32 import android.os.PowerManager;
     33 import android.provider.VoicemailContract;
     34 import android.support.v4.content.FileProvider;
     35 import android.util.Log;
     36 import android.view.WindowManager.LayoutParams;
     37 
     38 import com.android.dialer.R;
     39 import com.android.dialer.calllog.CallLogAsyncTaskUtil;
     40 import com.android.dialer.util.AsyncTaskExecutor;
     41 import com.android.dialer.util.AsyncTaskExecutors;
     42 import com.android.common.io.MoreCloseables;
     43 
     44 import java.io.File;
     45 import java.io.IOException;
     46 import java.util.ArrayList;
     47 import java.util.List;
     48 import java.util.concurrent.Executors;
     49 import java.util.concurrent.RejectedExecutionException;
     50 import java.util.concurrent.ScheduledExecutorService;
     51 import java.util.concurrent.TimeUnit;
     52 import java.util.concurrent.atomic.AtomicBoolean;
     53 import java.util.concurrent.atomic.AtomicInteger;
     54 
     55 import javax.annotation.concurrent.NotThreadSafe;
     56 import 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
     72 public 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