• 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.LogUtil;
51 import com.android.dialer.common.concurrent.AsyncTaskExecutor;
52 import com.android.dialer.common.concurrent.AsyncTaskExecutors;
53 import com.android.dialer.common.concurrent.DialerExecutor;
54 import com.android.dialer.common.concurrent.DialerExecutors;
55 import com.android.dialer.configprovider.ConfigProviderBindings;
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.telecom.TelecomUtil;
61 import com.android.dialer.util.PermissionsUtil;
62 import com.google.common.io.ByteStreams;
63 import java.io.File;
64 import java.io.IOException;
65 import java.io.InputStream;
66 import java.io.OutputStream;
67 import java.text.SimpleDateFormat;
68 import java.util.Date;
69 import java.util.Locale;
70 import java.util.concurrent.Executors;
71 import java.util.concurrent.RejectedExecutionException;
72 import java.util.concurrent.ScheduledExecutorService;
73 import java.util.concurrent.atomic.AtomicBoolean;
74 import java.util.concurrent.atomic.AtomicInteger;
75 import javax.annotation.concurrent.NotThreadSafe;
76 import javax.annotation.concurrent.ThreadSafe;
77 
78 /**
79  * Contains the controlling logic for a voicemail playback in the call log. It is closely coupled to
80  * assumptions about the behaviors and lifecycle of the call log, in particular in the {@link
81  * CallLogFragment} and {@link CallLogAdapter}.
82  *
83  * <p>This controls a single {@link com.android.dialer.app.voicemail.VoicemailPlaybackLayout}. A
84  * single instance can be reused for different such layouts, using {@link #setPlaybackView}. This is
85  * to facilitate reuse across different voicemail call log entries.
86  *
87  * <p>This class is not thread safe. The thread policy for this class is thread-confinement, all
88  * calls into this class from outside must be done from the main UI thread.
89  */
90 @NotThreadSafe
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 || mContext == 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     if (mContext != null && TelecomUtil.isInCall(mContext)) {
520       handleError(new IllegalStateException("Cannot play voicemail when call is in progress"));
521       return;
522     }
523 
524     try {
525       mMediaPlayer = new MediaPlayer();
526       mMediaPlayer.setOnPreparedListener(this);
527       mMediaPlayer.setOnErrorListener(this);
528       mMediaPlayer.setOnCompletionListener(this);
529 
530       mMediaPlayer.reset();
531       mMediaPlayer.setDataSource(mContext, mVoicemailUri);
532       mMediaPlayer.setAudioStreamType(VoicemailAudioManager.PLAYBACK_STREAM);
533       mMediaPlayer.prepareAsync();
534     } catch (IOException e) {
535       handleError(e);
536     }
537   }
538 
539   /**
540    * Once the media player is prepared, enables the UI and adopts the appropriate playback state.
541    */
542   @Override
onPrepared(MediaPlayer mp)543   public void onPrepared(MediaPlayer mp) {
544     if (mView == null || mContext == null) {
545       return;
546     }
547     LogUtil.d("VoicemailPlaybackPresenter.onPrepared", null);
548     mIsPrepared = true;
549 
550     mDuration.set(mMediaPlayer.getDuration());
551 
552     LogUtil.d("VoicemailPlaybackPresenter.onPrepared", "mPosition=" + mPosition);
553     mView.setClipPosition(mPosition, mDuration.get());
554     mView.enableUiElements();
555     mView.setSuccess();
556     if (!mp.isPlaying()) {
557       mMediaPlayer.seekTo(mPosition);
558     }
559 
560     if (mIsPlaying) {
561       resumePlayback();
562     } else {
563       pausePlayback();
564     }
565   }
566 
567   /**
568    * Invoked if preparing the media player fails, for example, if file is missing or the voicemail
569    * is an unknown file format that can't be played.
570    */
571   @Override
onError(MediaPlayer mp, int what, int extra)572   public boolean onError(MediaPlayer mp, int what, int extra) {
573     handleError(new IllegalStateException("MediaPlayer error listener invoked: " + extra));
574     return true;
575   }
576 
handleError(Exception e)577   protected void handleError(Exception e) {
578     LogUtil.e("VoicemailPlaybackPresenter.handlerError", "could not play voicemail", e);
579 
580     if (mIsPrepared) {
581       mMediaPlayer.release();
582       mMediaPlayer = null;
583       mIsPrepared = false;
584     }
585 
586     if (mView != null) {
587       mView.onPlaybackError();
588     }
589 
590     mPosition = 0;
591     mIsPlaying = false;
592     showShareVoicemailButton(false);
593   }
594 
595   /** After done playing the voicemail clip, reset the clip position to the start. */
596   @Override
onCompletion(MediaPlayer mediaPlayer)597   public void onCompletion(MediaPlayer mediaPlayer) {
598     pausePlayback();
599 
600     // Reset the seekbar position to the beginning.
601     mPosition = 0;
602     if (mView != null) {
603       mediaPlayer.seekTo(0);
604       mView.setClipPosition(0, mDuration.get());
605     }
606   }
607 
608   /**
609    * Only play voicemail when audio focus is granted. When it is lost (usually by another
610    * application requesting focus), pause playback. Audio focus gain/lost only triggers the focus is
611    * requested. Audio focus is requested when the user pressed play and abandoned when the user
612    * pressed pause or the audio has finished. Losing focus should not abandon focus as the voicemail
613    * should resume once the focus is returned.
614    *
615    * @param gainedFocus {@code true} if the audio focus was gained, {@code} false otherwise.
616    */
onAudioFocusChange(boolean gainedFocus)617   public void onAudioFocusChange(boolean gainedFocus) {
618     if (mIsPlaying == gainedFocus) {
619       // Nothing new here, just exit.
620       return;
621     }
622 
623     if (gainedFocus) {
624       resumePlayback();
625     } else {
626       pausePlayback(true);
627     }
628   }
629 
630   /**
631    * Resumes voicemail playback at the clip position stored by the presenter. Null-op if already
632    * playing.
633    */
resumePlayback()634   public void resumePlayback() {
635     if (mView == null) {
636       return;
637     }
638 
639     if (!mIsPrepared) {
640       /*
641        * Check content before requesting content to avoid duplicated requests. It is possible
642        * that the UI doesn't know content has arrived if the fetch took too long causing a
643        * timeout, but succeeded.
644        */
645       checkForContent(
646           hasContent -> {
647             if (!hasContent) {
648               // No local content, download from server. Queue playing if the request was
649               // issued,
650               mIsPlaying = requestContent(PLAYBACK_REQUEST);
651             } else {
652               showShareVoicemailButton(true);
653               // Queue playing once the media play loaded the content.
654               mIsPlaying = true;
655               prepareContent();
656             }
657           });
658       return;
659     }
660 
661     mIsPlaying = true;
662 
663     mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
664 
665     if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) {
666       // Clamp the start position between 0 and the duration.
667       mPosition = Math.max(0, Math.min(mPosition, mDuration.get()));
668 
669       mMediaPlayer.seekTo(mPosition);
670 
671       try {
672         // Grab audio focus.
673         // Can throw RejectedExecutionException.
674         mVoicemailAudioManager.requestAudioFocus();
675         mMediaPlayer.start();
676         setSpeakerphoneOn(mIsSpeakerphoneOn);
677         mVoicemailAudioManager.setSpeakerphoneOn(mIsSpeakerphoneOn);
678       } catch (RejectedExecutionException e) {
679         handleError(e);
680       }
681     }
682 
683     LogUtil.d("VoicemailPlaybackPresenter.resumePlayback", "resumed playback at %d.", mPosition);
684     mView.onPlaybackStarted(mDuration.get(), getScheduledExecutorServiceInstance());
685   }
686 
687   /** Pauses voicemail playback at the current position. Null-op if already paused. */
pausePlayback()688   public void pausePlayback() {
689     pausePlayback(false);
690   }
691 
pausePlayback(boolean keepFocus)692   private void pausePlayback(boolean keepFocus) {
693     if (!mIsPrepared) {
694       return;
695     }
696 
697     mIsPlaying = false;
698 
699     if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
700       mMediaPlayer.pause();
701     }
702 
703     mPosition = mMediaPlayer == null ? 0 : mMediaPlayer.getCurrentPosition();
704 
705     LogUtil.d("VoicemailPlaybackPresenter.pausePlayback", "paused playback at %d.", mPosition);
706 
707     if (mView != null) {
708       mView.onPlaybackStopped();
709     }
710 
711     if (!keepFocus) {
712       mVoicemailAudioManager.abandonAudioFocus();
713     }
714     if (mActivity != null) {
715       mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
716     }
717     disableProximitySensor(true /* waitForFarState */);
718   }
719 
720   /**
721    * Pauses playback when the user starts seeking the position, and notes whether the voicemail is
722    * playing to know whether to resume playback once the user selects a new position.
723    */
pausePlaybackForSeeking()724   public void pausePlaybackForSeeking() {
725     if (mMediaPlayer != null) {
726       mShouldResumePlaybackAfterSeeking = mMediaPlayer.isPlaying();
727     }
728     pausePlayback(true);
729   }
730 
resumePlaybackAfterSeeking(int desiredPosition)731   public void resumePlaybackAfterSeeking(int desiredPosition) {
732     mPosition = desiredPosition;
733     if (mShouldResumePlaybackAfterSeeking) {
734       mShouldResumePlaybackAfterSeeking = false;
735       resumePlayback();
736     }
737   }
738 
739   /**
740    * Seek to position. This is called when user manually seek the playback. It could be either by
741    * touch or volume button while in talkback mode.
742    */
seek(int position)743   public void seek(int position) {
744     mPosition = position;
745     mMediaPlayer.seekTo(mPosition);
746   }
747 
enableProximitySensor()748   private void enableProximitySensor() {
749     if (mProximityWakeLock == null
750         || mIsSpeakerphoneOn
751         || !mIsPrepared
752         || mMediaPlayer == null
753         || !mMediaPlayer.isPlaying()) {
754       return;
755     }
756 
757     if (!mProximityWakeLock.isHeld()) {
758       LogUtil.i(
759           "VoicemailPlaybackPresenter.enableProximitySensor", "acquiring proximity wake lock");
760       mProximityWakeLock.acquire();
761     } else {
762       LogUtil.i(
763           "VoicemailPlaybackPresenter.enableProximitySensor",
764           "proximity wake lock already acquired");
765     }
766   }
767 
disableProximitySensor(boolean waitForFarState)768   private void disableProximitySensor(boolean waitForFarState) {
769     if (mProximityWakeLock == null) {
770       return;
771     }
772     if (mProximityWakeLock.isHeld()) {
773       LogUtil.i(
774           "VoicemailPlaybackPresenter.disableProximitySensor", "releasing proximity wake lock");
775       int flags = waitForFarState ? PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY : 0;
776       mProximityWakeLock.release(flags);
777     } else {
778       LogUtil.i(
779           "VoicemailPlaybackPresenter.disableProximitySensor",
780           "proximity wake lock already released");
781     }
782   }
783 
784   /** This is for use by UI interactions only. It simplifies UI logic. */
toggleSpeakerphone()785   public void toggleSpeakerphone() {
786     mVoicemailAudioManager.setSpeakerphoneOn(!mIsSpeakerphoneOn);
787     setSpeakerphoneOn(!mIsSpeakerphoneOn);
788   }
789 
setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener)790   public void setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener) {
791     mOnVoicemailDeletedListener = listener;
792   }
793 
getMediaPlayerPosition()794   public int getMediaPlayerPosition() {
795     return mIsPrepared && mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : 0;
796   }
797 
onVoicemailDeleted(CallLogListItemViewHolder viewHolder)798   void onVoicemailDeleted(CallLogListItemViewHolder viewHolder) {
799     if (mOnVoicemailDeletedListener != null) {
800       mOnVoicemailDeletedListener.onVoicemailDeleted(viewHolder, mVoicemailUri);
801     }
802   }
803 
onVoicemailDeleteUndo(int adapterPosition)804   void onVoicemailDeleteUndo(int adapterPosition) {
805     if (mOnVoicemailDeletedListener != null) {
806       mOnVoicemailDeletedListener.onVoicemailDeleteUndo(mRowId, adapterPosition, mVoicemailUri);
807     }
808   }
809 
onVoicemailDeletedInDatabase()810   void onVoicemailDeletedInDatabase() {
811     if (mOnVoicemailDeletedListener != null) {
812       mOnVoicemailDeletedListener.onVoicemailDeletedInDatabase(mRowId, mVoicemailUri);
813     }
814   }
815 
816   @VisibleForTesting
isPlaying()817   public boolean isPlaying() {
818     return mIsPlaying;
819   }
820 
821   @VisibleForTesting
isSpeakerphoneOn()822   public boolean isSpeakerphoneOn() {
823     return mIsSpeakerphoneOn;
824   }
825 
826   /**
827    * This method only handles app-level changes to the speakerphone. Audio layer changes should be
828    * handled separately. This is so that the VoicemailAudioManager can trigger changes to the
829    * presenter without the presenter triggering the audio manager and duplicating actions.
830    */
setSpeakerphoneOn(boolean on)831   public void setSpeakerphoneOn(boolean on) {
832     if (mView == null) {
833       return;
834     }
835 
836     mView.onSpeakerphoneOn(on);
837 
838     mIsSpeakerphoneOn = on;
839 
840     // This should run even if speakerphone is not being toggled because we may be switching
841     // from earpiece to headphone and vise versa. Also upon initial setup the default audio
842     // source is the earpiece, so we want to trigger the proximity sensor.
843     if (mIsPlaying) {
844       if (on || mVoicemailAudioManager.isWiredHeadsetPluggedIn()) {
845         disableProximitySensor(false /* waitForFarState */);
846       } else {
847         enableProximitySensor();
848       }
849     }
850   }
851 
852   @VisibleForTesting
clearInstance()853   public void clearInstance() {
854     sInstance = null;
855   }
856 
showShareVoicemailButton(boolean show)857   private void showShareVoicemailButton(boolean show) {
858     if (mContext == null) {
859       return;
860     }
861     if (isShareVoicemailAllowed(mContext) && shareVoicemailButtonView != null) {
862       if (show) {
863         Logger.get(mContext).logImpression(DialerImpression.Type.VVM_SHARE_VISIBLE);
864       }
865       LogUtil.d("VoicemailPlaybackPresenter.showShareVoicemailButton", "show: %b", show);
866       shareVoicemailButtonView.setVisibility(show ? View.VISIBLE : View.GONE);
867     }
868   }
869 
isShareVoicemailAllowed(Context context)870   private static boolean isShareVoicemailAllowed(Context context) {
871     return ConfigProviderBindings.get(context).getBoolean(CONFIG_SHARE_VOICEMAIL_ALLOWED, true);
872   }
873 
874   private static class ShareVoicemailWorker
875       implements DialerExecutor.Worker<Pair<Context, Uri>, Pair<Uri, String>> {
876 
877     @Nullable
878     @Override
doInBackground(Pair<Context, Uri> input)879     public Pair<Uri, String> doInBackground(Pair<Context, Uri> input) {
880       Context context = input.first;
881       Uri voicemailUri = input.second;
882       ContentResolver contentResolver = context.getContentResolver();
883       try (Cursor callLogInfo = getCallLogInfoCursor(contentResolver, voicemailUri);
884           Cursor contentInfo = getContentInfoCursor(contentResolver, voicemailUri)) {
885 
886         if (hasContent(callLogInfo) && hasContent(contentInfo)) {
887           String cachedName = callLogInfo.getString(CallLogQuery.CACHED_NAME);
888           String number = contentInfo.getString(contentInfo.getColumnIndex(Voicemails.NUMBER));
889           long date = contentInfo.getLong(contentInfo.getColumnIndex(Voicemails.DATE));
890           String mimeType = contentInfo.getString(contentInfo.getColumnIndex(Voicemails.MIME_TYPE));
891           String transcription =
892               contentInfo.getString(contentInfo.getColumnIndex(Voicemails.TRANSCRIPTION));
893 
894           // Copy voicemail content to a new file.
895           // Please see reference in third_party/java_src/android_app/dialer/java/com/android/
896           // dialer/app/res/xml/file_paths.xml for correct cache directory name.
897           File parentDir = new File(context.getCacheDir(), "my_cache");
898           if (!parentDir.exists()) {
899             parentDir.mkdirs();
900           }
901           File temporaryVoicemailFile =
902               new File(parentDir, getFileName(cachedName, number, mimeType, date));
903 
904           try (InputStream inputStream = contentResolver.openInputStream(voicemailUri);
905               OutputStream outputStream =
906                   contentResolver.openOutputStream(Uri.fromFile(temporaryVoicemailFile))) {
907             if (inputStream != null && outputStream != null) {
908               ByteStreams.copy(inputStream, outputStream);
909               return new Pair<>(
910                   FileProvider.getUriForFile(
911                       context, Constants.get().getFileProviderAuthority(), temporaryVoicemailFile),
912                   transcription);
913             }
914           } catch (IOException e) {
915             LogUtil.e(
916                 "VoicemailAsyncTaskUtil.shareVoicemail",
917                 "failed to copy voicemail content to new file: ",
918                 e);
919           }
920           return null;
921         }
922       }
923       return null;
924     }
925   }
926 
927   /**
928    * Share voicemail to be opened by user selected apps. This method will collect information, copy
929    * voicemail to a temporary file in background and launch a chooser intent to share it.
930    */
shareVoicemail()931   public void shareVoicemail() {
932     shareVoicemailExecutor.executeParallel(new Pair<>(mContext, mVoicemailUri));
933   }
934 
getFileName(String cachedName, String number, String mimeType, long date)935   private static String getFileName(String cachedName, String number, String mimeType, long date) {
936     String callerName = TextUtils.isEmpty(cachedName) ? number : cachedName;
937     SimpleDateFormat simpleDateFormat =
938         new SimpleDateFormat(VOICEMAIL_SHARE_FILE_NAME_DATE_FORMAT, Locale.getDefault());
939 
940     String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
941 
942     return callerName
943         + "_"
944         + simpleDateFormat.format(new Date(date))
945         + (TextUtils.isEmpty(fileExtension) ? "" : "." + fileExtension);
946   }
947 
getShareIntent( Context context, Uri voicemailFileUri, String transcription)948   private static Intent getShareIntent(
949       Context context, Uri voicemailFileUri, String transcription) {
950     Intent shareIntent = new Intent();
951     if (TextUtils.isEmpty(transcription)) {
952       shareIntent.setAction(Intent.ACTION_SEND);
953       shareIntent.putExtra(Intent.EXTRA_STREAM, voicemailFileUri);
954       shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
955       shareIntent.setType(context.getContentResolver().getType(voicemailFileUri));
956     } else {
957       shareIntent.setAction(Intent.ACTION_SEND);
958       shareIntent.putExtra(Intent.EXTRA_STREAM, voicemailFileUri);
959       shareIntent.putExtra(Intent.EXTRA_TEXT, transcription);
960       shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
961       shareIntent.setType("*/*");
962     }
963 
964     return shareIntent;
965   }
966 
hasContent(@ullable Cursor cursor)967   private static boolean hasContent(@Nullable Cursor cursor) {
968     return cursor != null && cursor.moveToFirst();
969   }
970 
971   @Nullable
getCallLogInfoCursor(ContentResolver contentResolver, Uri voicemailUri)972   private static Cursor getCallLogInfoCursor(ContentResolver contentResolver, Uri voicemailUri) {
973     return contentResolver.query(
974         ContentUris.withAppendedId(
975             CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL, ContentUris.parseId(voicemailUri)),
976         CallLogQuery.getProjection(),
977         null,
978         null,
979         null);
980   }
981 
982   @Nullable
getContentInfoCursor(ContentResolver contentResolver, Uri voicemailUri)983   private static Cursor getContentInfoCursor(ContentResolver contentResolver, Uri voicemailUri) {
984     return contentResolver.query(
985         voicemailUri,
986         new String[] {
987           Voicemails._ID,
988           Voicemails.NUMBER,
989           Voicemails.DATE,
990           Voicemails.MIME_TYPE,
991           Voicemails.TRANSCRIPTION,
992         },
993         null,
994         null,
995         null);
996   }
997 
998   /** The enumeration of {@link AsyncTask} objects we use in this class. */
999   public enum Tasks {
1000     CHECK_FOR_CONTENT,
1001     CHECK_CONTENT_AFTER_CHANGE,
1002     SHARE_VOICEMAIL,
1003     SEND_FETCH_REQUEST
1004   }
1005 
1006   /** Contract describing the behaviour we need from the ui we are controlling. */
1007   public interface PlaybackView {
1008 
getDesiredClipPosition()1009     int getDesiredClipPosition();
1010 
disableUiElements()1011     void disableUiElements();
1012 
enableUiElements()1013     void enableUiElements();
1014 
onPlaybackError()1015     void onPlaybackError();
1016 
onPlaybackStarted(int duration, ScheduledExecutorService executorService)1017     void onPlaybackStarted(int duration, ScheduledExecutorService executorService);
1018 
onPlaybackStopped()1019     void onPlaybackStopped();
1020 
onSpeakerphoneOn(boolean on)1021     void onSpeakerphoneOn(boolean on);
1022 
setClipPosition(int clipPositionInMillis, int clipLengthInMillis)1023     void setClipPosition(int clipPositionInMillis, int clipLengthInMillis);
1024 
setSuccess()1025     void setSuccess();
1026 
setFetchContentTimeout()1027     void setFetchContentTimeout();
1028 
setIsFetchingContent()1029     void setIsFetchingContent();
1030 
setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri)1031     void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri);
1032 
resetSeekBar()1033     void resetSeekBar();
1034   }
1035 
1036   public interface OnVoicemailDeletedListener {
1037 
onVoicemailDeleted(CallLogListItemViewHolder viewHolder, Uri uri)1038     void onVoicemailDeleted(CallLogListItemViewHolder viewHolder, Uri uri);
1039 
onVoicemailDeleteUndo(long rowId, int adaptorPosition, Uri uri)1040     void onVoicemailDeleteUndo(long rowId, int adaptorPosition, Uri uri);
1041 
onVoicemailDeletedInDatabase(long rowId, Uri uri)1042     void onVoicemailDeletedInDatabase(long rowId, Uri uri);
1043   }
1044 
1045   protected interface OnContentCheckedListener {
1046 
onContentChecked(boolean hasContent)1047     void onContentChecked(boolean hasContent);
1048   }
1049 
1050   @ThreadSafe
1051   private class FetchResultHandler extends ContentObserver implements Runnable {
1052 
1053     private final Handler mFetchResultHandler;
1054     private final Uri mVoicemailUri;
1055     private AtomicBoolean mIsWaitingForResult = new AtomicBoolean(true);
1056 
FetchResultHandler(Handler handler, Uri uri, int code)1057     public FetchResultHandler(Handler handler, Uri uri, int code) {
1058       super(handler);
1059       mFetchResultHandler = handler;
1060       mVoicemailUri = uri;
1061       if (mContext != null) {
1062         if (PermissionsUtil.hasReadVoicemailPermissions(mContext)) {
1063           mContext.getContentResolver().registerContentObserver(mVoicemailUri, false, this);
1064         }
1065         mFetchResultHandler.postDelayed(this, FETCH_CONTENT_TIMEOUT_MS);
1066       }
1067     }
1068 
1069     /** Stop waiting for content and notify UI if {@link FETCH_CONTENT_TIMEOUT_MS} has elapsed. */
1070     @Override
run()1071     public void run() {
1072       if (mIsWaitingForResult.getAndSet(false) && mContext != null) {
1073         mContext.getContentResolver().unregisterContentObserver(this);
1074         if (mView != null) {
1075           mView.setFetchContentTimeout();
1076         }
1077       }
1078     }
1079 
destroy()1080     public void destroy() {
1081       if (mIsWaitingForResult.getAndSet(false) && mContext != null) {
1082         mContext.getContentResolver().unregisterContentObserver(this);
1083         mFetchResultHandler.removeCallbacks(this);
1084       }
1085     }
1086 
1087     @Override
onChange(boolean selfChange)1088     public void onChange(boolean selfChange) {
1089       mAsyncTaskExecutor.submit(
1090           Tasks.CHECK_CONTENT_AFTER_CHANGE,
1091           new AsyncTask<Void, Void, Boolean>() {
1092 
1093             @Override
1094             public Boolean doInBackground(Void... params) {
1095               return queryHasContent(mVoicemailUri);
1096             }
1097 
1098             @Override
1099             public void onPostExecute(Boolean hasContent) {
1100               if (hasContent && mContext != null && mIsWaitingForResult.getAndSet(false)) {
1101                 mContext.getContentResolver().unregisterContentObserver(FetchResultHandler.this);
1102                 showShareVoicemailButton(true);
1103                 prepareContent();
1104               }
1105             }
1106           });
1107     }
1108   }
1109 }
1110