• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.dialer.voicemail;
18 
19 import 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 {
getDesiredClipPosition()79         int getDesiredClipPosition();
disableUiElements()80         void disableUiElements();
enableUiElements()81         void enableUiElements();
onPlaybackError()82         void onPlaybackError();
onPlaybackStarted(int duration, ScheduledExecutorService executorService)83         void onPlaybackStarted(int duration, ScheduledExecutorService executorService);
onPlaybackStopped()84         void onPlaybackStopped();
onSpeakerphoneOn(boolean on)85         void onSpeakerphoneOn(boolean on);
setClipPosition(int clipPositionInMillis, int clipLengthInMillis)86         void setClipPosition(int clipPositionInMillis, int clipLengthInMillis);
setSuccess()87         void setSuccess();
setFetchContentTimeout()88         void setFetchContentTimeout();
setIsFetchingContent()89         void setIsFetchingContent();
onVoicemailArchiveSucceded(Uri voicemailUri)90         void onVoicemailArchiveSucceded(Uri voicemailUri);
onVoicemailArchiveFailed(Uri voicemailUri)91         void onVoicemailArchiveFailed(Uri voicemailUri);
setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri)92         void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri);
resetSeekBar()93         void resetSeekBar();
94     }
95 
96     public interface OnVoicemailDeletedListener {
onVoicemailDeleted(Uri uri)97         void onVoicemailDeleted(Uri uri);
onVoicemailDeleteUndo()98         void onVoicemailDeleteUndo();
onVoicemailDeletedInDatabase()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 {
onContentChecked(boolean hasContent)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      */
getInstance( Activity activity, Bundle savedInstanceState)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      */
VoicemailPlaybackPresenter(Activity activity)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      */
init(Activity activity, Bundle savedInstanceState)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      */
onSaveInstanceState(Bundle outState)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      */
setPlaybackView( PlaybackView view, Uri voicemailUri, boolean startPlayingImmediately)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      */
resetAll()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      */
pausePresenter(boolean reset)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      */
onResume()352     public void onResume() {
353         mVoicemailAudioManager.registerReceivers();
354     }
355 
356     /**
357      * Must be invoked when the parent activity is paused.
358      */
onPause()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      */
onDestroy()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      */
checkForContent(final OnContentCheckedListener callback)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 
queryHasContent(Uri voicemailUri)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      */
requestContent(int code)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 
FetchResultHandler(Handler handler, Uri uri, int code)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
run()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 
destroy()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
onChange(boolean selfChange)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      */
prepareContent()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
onPrepared(MediaPlayer mp)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
onError(MediaPlayer mp, int what, int extra)625     public boolean onError(MediaPlayer mp, int what, int extra) {
626         handleError(new IllegalStateException("MediaPlayer error listener invoked: " + extra));
627         return true;
628     }
629 
handleError(Exception e)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
onCompletion(MediaPlayer mediaPlayer)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      */
onAudioFocusChange(boolean gainedFocus)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      */
resumePlayback()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      */
pausePlayback()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      */
pausePlaybackForSeeking()769     public void pausePlaybackForSeeking() {
770         if (mMediaPlayer != null) {
771             mShouldResumePlaybackAfterSeeking = mMediaPlayer.isPlaying();
772         }
773         pausePlayback();
774     }
775 
resumePlaybackAfterSeeking(int desiredPosition)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      */
seek(int position)789     public void seek(int position) {
790         mPosition = position;
791     }
792 
enableProximitySensor()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 
disableProximitySensor(boolean waitForFarState)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      */
toggleSpeakerphone()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      */
setSpeakerphoneOn(boolean on)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 
setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener)860     public void setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener) {
861         mOnVoicemailDeletedListener = listener;
862     }
863 
getMediaPlayerPosition()864     public int getMediaPlayerPosition() {
865         return mIsPrepared && mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : 0;
866     }
867 
notifyUiOfArchiveResult(Uri voicemailUri, boolean archived)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 
onVoicemailDeleted()879     /* package */ void onVoicemailDeleted() {
880         // Trampoline the event notification to the interested listener.
881         if (mOnVoicemailDeletedListener != null) {
882             mOnVoicemailDeletedListener.onVoicemailDeleted(mVoicemailUri);
883         }
884     }
885 
onVoicemailDeleteUndo()886     /* package */ void onVoicemailDeleteUndo() {
887         // Trampoline the event notification to the interested listener.
888         if (mOnVoicemailDeletedListener != null) {
889             mOnVoicemailDeletedListener.onVoicemailDeleteUndo();
890         }
891     }
892 
onVoicemailDeletedInDatabase()893     /* package */ void onVoicemailDeletedInDatabase() {
894         // Trampoline the event notification to the interested listener.
895         if (mOnVoicemailDeletedListener != null) {
896             mOnVoicemailDeletedListener.onVoicemailDeletedInDatabase();
897         }
898     }
899 
getScheduledExecutorServiceInstance()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      */
archiveContent(final Uri voicemailUri, final boolean archivedByUser)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      */
startArchiveVoicemailTask(final Uri voicemailUri, final boolean archivedByUser)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      */
sendShareIntent(final Uri voicemailUri)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. */
setArchivedVoicemailStatusAndUpdateUI( final Uri voicemailUri, final Uri archivedVoicemailUri, boolean status)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 
getShareIntent(Uri voicemailFileUri)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
isPlaying()997     public boolean isPlaying() {
998         return mIsPlaying;
999     }
1000 
1001     @VisibleForTesting
isSpeakerphoneOn()1002     public boolean isSpeakerphoneOn() {
1003         return mIsSpeakerphoneOn;
1004     }
1005 
1006     @VisibleForTesting
clearInstance()1007     public void clearInstance() {
1008         sInstance = null;
1009     }
1010 }
1011