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