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