• 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.app.Activity;
20 import android.content.ContentResolver;
21 import android.content.ContentUris;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.database.ContentObserver;
25 import android.database.Cursor;
26 import android.media.AudioManager;
27 import android.media.MediaPlayer;
28 import android.net.Uri;
29 import android.os.AsyncTask;
30 import android.os.Bundle;
31 import android.os.Handler;
32 import android.os.PowerManager;
33 import android.provider.CallLog;
34 import android.provider.VoicemailContract;
35 import android.provider.VoicemailContract.Voicemails;
36 import android.support.annotation.MainThread;
37 import android.support.annotation.Nullable;
38 import android.support.annotation.VisibleForTesting;
39 import android.support.v4.content.FileProvider;
40 import android.text.TextUtils;
41 import android.util.Pair;
42 import android.view.View;
43 import android.view.WindowManager.LayoutParams;
44 import android.webkit.MimeTypeMap;
45 import com.android.common.io.MoreCloseables;
46 import com.android.dialer.app.R;
47 import com.android.dialer.app.calllog.CallLogListItemViewHolder;
48 import com.android.dialer.common.Assert;
49 import com.android.dialer.common.LogUtil;
50 import com.android.dialer.common.concurrent.AsyncTaskExecutor;
51 import com.android.dialer.common.concurrent.AsyncTaskExecutors;
52 import com.android.dialer.common.concurrent.DialerExecutor;
53 import com.android.dialer.common.concurrent.DialerExecutorComponent;
54 import com.android.dialer.configprovider.ConfigProviderComponent;
55 import com.android.dialer.constants.Constants;
56 import com.android.dialer.logging.DialerImpression;
57 import com.android.dialer.logging.Logger;
58 import com.android.dialer.phonenumbercache.CallLogQuery;
59 import com.android.dialer.strictmode.StrictModeUtils;
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 public class VoicemailPlaybackPresenter
92     implements MediaPlayer.OnPreparedListener,
93         MediaPlayer.OnCompletionListener,
94         MediaPlayer.OnErrorListener {
95 
96   public static final int PLAYBACK_REQUEST = 0;
97   private static final int NUMBER_OF_THREADS_IN_POOL = 2;
98   // Time to wait for content to be fetched before timing out.
99   private static final long FETCH_CONTENT_TIMEOUT_MS = 20000;
100   private static final String VOICEMAIL_URI_KEY =
101       VoicemailPlaybackPresenter.class.getName() + ".VOICEMAIL_URI";
102   private static final String IS_PREPARED_KEY =
103       VoicemailPlaybackPresenter.class.getName() + ".IS_PREPARED";
104   // If present in the saved instance bundle, we should not resume playback on create.
105   private static final String IS_PLAYING_STATE_KEY =
106       VoicemailPlaybackPresenter.class.getName() + ".IS_PLAYING_STATE_KEY";
107   // If present in the saved instance bundle, indicates where to set the playback slider.
108   private static final String CLIP_POSITION_KEY =
109       VoicemailPlaybackPresenter.class.getName() + ".CLIP_POSITION_KEY";
110   private static final String IS_SPEAKERPHONE_ON_KEY =
111       VoicemailPlaybackPresenter.class.getName() + ".IS_SPEAKER_PHONE_ON";
112   private static final String VOICEMAIL_SHARE_FILE_NAME_DATE_FORMAT = "MM-dd-yy_hhmmaa";
113   private static final String CONFIG_SHARE_VOICEMAIL_ALLOWED = "share_voicemail_allowed";
114 
115   private static VoicemailPlaybackPresenter instance;
116   private static ScheduledExecutorService scheduledExecutorService;
117   /**
118    * The most recently cached duration. We cache this since we don't want to keep requesting it from
119    * the player, as this can easily lead to throwing {@link IllegalStateException} (any time the
120    * player is released, it's illegal to ask for the duration).
121    */
122   private final AtomicInteger duration = new AtomicInteger(0);
123 
124   protected Context context;
125   private long rowId;
126   protected Uri voicemailUri;
127   protected MediaPlayer mediaPlayer;
128   // Used to run async tasks that need to interact with the UI.
129   protected AsyncTaskExecutor asyncTaskExecutor;
130   private Activity activity;
131   private PlaybackView view;
132   private int position;
133   private boolean isPlaying;
134   // MediaPlayer crashes on some method calls if not prepared but does not have a method which
135   // exposes its prepared state. Store this locally, so we can check and prevent crashes.
136   private boolean isPrepared;
137   private boolean isSpeakerphoneOn;
138 
139   private boolean shouldResumePlaybackAfterSeeking;
140   /**
141    * Used to handle the result of a successful or time-out fetch result.
142    *
143    * <p>This variable is thread-contained, accessed only on the ui thread.
144    */
145   private FetchResultHandler fetchResultHandler;
146 
147   private PowerManager.WakeLock proximityWakeLock;
148   private VoicemailAudioManager voicemailAudioManager;
149   private OnVoicemailDeletedListener onVoicemailDeletedListener;
150   private View shareVoicemailButtonView;
151 
152   private DialerExecutor<Pair<Context, Uri>> shareVoicemailExecutor;
153 
154   /** Initialize variables which are activity-independent and state-independent. */
VoicemailPlaybackPresenter(Activity activity)155   protected VoicemailPlaybackPresenter(Activity activity) {
156     Context context = activity.getApplicationContext();
157     asyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor();
158     voicemailAudioManager = new VoicemailAudioManager(context, this);
159     PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
160     if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
161       proximityWakeLock =
162           powerManager.newWakeLock(
163               PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, "VoicemailPlaybackPresenter");
164     }
165   }
166 
167   /**
168    * Obtain singleton instance of this class. Use a single instance to provide a consistent listener
169    * to the AudioManager when requesting and abandoning audio focus.
170    *
171    * <p>Otherwise, after rotation the previous listener will still be active but a new listener will
172    * be provided to calls to the AudioManager, which is bad. For example, abandoning audio focus
173    * with the new listeners results in an AUDIO_FOCUS_GAIN callback to the previous listener, which
174    * is the opposite of the intended behavior.
175    */
176   @MainThread
getInstance( Activity activity, Bundle savedInstanceState)177   public static VoicemailPlaybackPresenter getInstance(
178       Activity activity, Bundle savedInstanceState) {
179     if (instance == null) {
180       instance = new VoicemailPlaybackPresenter(activity);
181     }
182 
183     instance.init(activity, savedInstanceState);
184     return instance;
185   }
186 
getScheduledExecutorServiceInstance()187   private static synchronized ScheduledExecutorService getScheduledExecutorServiceInstance() {
188     if (scheduledExecutorService == null) {
189       scheduledExecutorService = Executors.newScheduledThreadPool(NUMBER_OF_THREADS_IN_POOL);
190     }
191     return scheduledExecutorService;
192   }
193 
194   /** Update variables which are activity-dependent or state-dependent. */
195   @MainThread
init(Activity activity, Bundle savedInstanceState)196   protected void init(Activity activity, Bundle savedInstanceState) {
197     Assert.isMainThread();
198     this.activity = activity;
199     context = activity;
200 
201     if (savedInstanceState != null) {
202       // Restores playback state when activity is recreated, such as after rotation.
203       voicemailUri = savedInstanceState.getParcelable(VOICEMAIL_URI_KEY);
204       isPrepared = savedInstanceState.getBoolean(IS_PREPARED_KEY);
205       position = savedInstanceState.getInt(CLIP_POSITION_KEY, 0);
206       isPlaying = savedInstanceState.getBoolean(IS_PLAYING_STATE_KEY, false);
207       isSpeakerphoneOn = savedInstanceState.getBoolean(IS_SPEAKERPHONE_ON_KEY, false);
208       AudioManager audioManager = activity.getSystemService(AudioManager.class);
209       audioManager.setSpeakerphoneOn(isSpeakerphoneOn);
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   }
595 
596   /** After done playing the voicemail clip, reset the clip position to the start. */
597   @Override
onCompletion(MediaPlayer mediaPlayer)598   public void onCompletion(MediaPlayer mediaPlayer) {
599     pausePlayback();
600 
601     // Reset the seekbar position to the beginning.
602     position = 0;
603     if (view != null) {
604       mediaPlayer.seekTo(0);
605       view.setClipPosition(0, duration.get());
606     }
607   }
608 
609   /**
610    * Only play voicemail when audio focus is granted. When it is lost (usually by another
611    * application requesting focus), pause playback. Audio focus gain/lost only triggers the focus is
612    * requested. Audio focus is requested when the user pressed play and abandoned when the user
613    * pressed pause or the audio has finished. Losing focus should not abandon focus as the voicemail
614    * should resume once the focus is returned.
615    *
616    * @param gainedFocus {@code true} if the audio focus was gained, {@code} false otherwise.
617    */
onAudioFocusChange(boolean gainedFocus)618   public void onAudioFocusChange(boolean gainedFocus) {
619     if (isPlaying == gainedFocus) {
620       // Nothing new here, just exit.
621       return;
622     }
623 
624     if (gainedFocus) {
625       resumePlayback();
626     } else {
627       pausePlayback(true);
628     }
629   }
630 
631   /**
632    * Resumes voicemail playback at the clip position stored by the presenter. Null-op if already
633    * playing.
634    */
resumePlayback()635   public void resumePlayback() {
636     if (view == null) {
637       return;
638     }
639 
640     if (!isPrepared) {
641       /*
642        * Check content before requesting content to avoid duplicated requests. It is possible
643        * that the UI doesn't know content has arrived if the fetch took too long causing a
644        * timeout, but succeeded.
645        */
646       checkForContent(
647           hasContent -> {
648             if (!hasContent) {
649               // No local content, download from server. Queue playing if the request was
650               // issued,
651               isPlaying = requestContent(PLAYBACK_REQUEST);
652             } else {
653               showShareVoicemailButton(true);
654               // Queue playing once the media play loaded the content.
655               isPlaying = true;
656               prepareContent();
657             }
658           });
659       return;
660     }
661 
662     isPlaying = true;
663 
664     activity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
665 
666     if (mediaPlayer != null && !mediaPlayer.isPlaying()) {
667       // Clamp the start position between 0 and the duration.
668       position = Math.max(0, Math.min(position, duration.get()));
669 
670       mediaPlayer.seekTo(position);
671 
672       try {
673         // Grab audio focus.
674         // Can throw RejectedExecutionException.
675         voicemailAudioManager.requestAudioFocus();
676         mediaPlayer.start();
677         setSpeakerphoneOn(isSpeakerphoneOn);
678         voicemailAudioManager.setSpeakerphoneOn(isSpeakerphoneOn);
679       } catch (RejectedExecutionException e) {
680         handleError(e);
681       }
682     }
683 
684     LogUtil.d("VoicemailPlaybackPresenter.resumePlayback", "resumed playback at %d.", position);
685     view.onPlaybackStarted(duration.get(), getScheduledExecutorServiceInstance());
686   }
687 
688   /** Pauses voicemail playback at the current position. Null-op if already paused. */
pausePlayback()689   public void pausePlayback() {
690     pausePlayback(false);
691   }
692 
pausePlayback(boolean keepFocus)693   private void pausePlayback(boolean keepFocus) {
694     if (!isPrepared) {
695       return;
696     }
697 
698     isPlaying = false;
699 
700     if (mediaPlayer != null && mediaPlayer.isPlaying()) {
701       mediaPlayer.pause();
702     }
703 
704     position = mediaPlayer == null ? 0 : mediaPlayer.getCurrentPosition();
705 
706     LogUtil.d("VoicemailPlaybackPresenter.pausePlayback", "paused playback at %d.", position);
707 
708     if (view != null) {
709       view.onPlaybackStopped();
710     }
711 
712     if (!keepFocus) {
713       voicemailAudioManager.abandonAudioFocus();
714     }
715     if (activity != null) {
716       activity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
717     }
718     disableProximitySensor(true /* waitForFarState */);
719   }
720 
721   /**
722    * Pauses playback when the user starts seeking the position, and notes whether the voicemail is
723    * playing to know whether to resume playback once the user selects a new position.
724    */
pausePlaybackForSeeking()725   public void pausePlaybackForSeeking() {
726     if (mediaPlayer != null) {
727       shouldResumePlaybackAfterSeeking = mediaPlayer.isPlaying();
728     }
729     pausePlayback(true);
730   }
731 
resumePlaybackAfterSeeking(int desiredPosition)732   public void resumePlaybackAfterSeeking(int desiredPosition) {
733     position = desiredPosition;
734     if (shouldResumePlaybackAfterSeeking) {
735       shouldResumePlaybackAfterSeeking = false;
736       resumePlayback();
737     }
738   }
739 
740   /**
741    * Seek to position. This is called when user manually seek the playback. It could be either by
742    * touch or volume button while in talkback mode.
743    */
seek(int position)744   public void seek(int position) {
745     this.position = position;
746     mediaPlayer.seekTo(this.position);
747   }
748 
enableProximitySensor()749   private void enableProximitySensor() {
750     if (proximityWakeLock == null
751         || isSpeakerphoneOn
752         || !isPrepared
753         || mediaPlayer == null
754         || !mediaPlayer.isPlaying()) {
755       return;
756     }
757 
758     if (!proximityWakeLock.isHeld()) {
759       LogUtil.i(
760           "VoicemailPlaybackPresenter.enableProximitySensor", "acquiring proximity wake lock");
761       proximityWakeLock.acquire();
762     } else {
763       LogUtil.i(
764           "VoicemailPlaybackPresenter.enableProximitySensor",
765           "proximity wake lock already acquired");
766     }
767   }
768 
disableProximitySensor(boolean waitForFarState)769   private void disableProximitySensor(boolean waitForFarState) {
770     if (proximityWakeLock == null) {
771       return;
772     }
773     if (proximityWakeLock.isHeld()) {
774       LogUtil.i(
775           "VoicemailPlaybackPresenter.disableProximitySensor", "releasing proximity wake lock");
776       int flags = waitForFarState ? PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY : 0;
777       proximityWakeLock.release(flags);
778     } else {
779       LogUtil.i(
780           "VoicemailPlaybackPresenter.disableProximitySensor",
781           "proximity wake lock already released");
782     }
783   }
784 
785   /** This is for use by UI interactions only. It simplifies UI logic. */
toggleSpeakerphone()786   public void toggleSpeakerphone() {
787     voicemailAudioManager.setSpeakerphoneOn(!isSpeakerphoneOn);
788     setSpeakerphoneOn(!isSpeakerphoneOn);
789   }
790 
setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener)791   public void setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener) {
792     onVoicemailDeletedListener = listener;
793   }
794 
getMediaPlayerPosition()795   public int getMediaPlayerPosition() {
796     return isPrepared && mediaPlayer != null ? mediaPlayer.getCurrentPosition() : 0;
797   }
798 
onVoicemailDeleted(CallLogListItemViewHolder viewHolder)799   void onVoicemailDeleted(CallLogListItemViewHolder viewHolder) {
800     if (onVoicemailDeletedListener != null) {
801       onVoicemailDeletedListener.onVoicemailDeleted(viewHolder, voicemailUri);
802     }
803   }
804 
onVoicemailDeleteUndo(int adapterPosition)805   void onVoicemailDeleteUndo(int adapterPosition) {
806     if (onVoicemailDeletedListener != null) {
807       onVoicemailDeletedListener.onVoicemailDeleteUndo(rowId, adapterPosition, voicemailUri);
808     }
809   }
810 
onVoicemailDeletedInDatabase()811   void onVoicemailDeletedInDatabase() {
812     if (onVoicemailDeletedListener != null) {
813       onVoicemailDeletedListener.onVoicemailDeletedInDatabase(rowId, voicemailUri);
814     }
815   }
816 
817   @VisibleForTesting
isPlaying()818   public boolean isPlaying() {
819     return isPlaying;
820   }
821 
822   @VisibleForTesting
isSpeakerphoneOn()823   public boolean isSpeakerphoneOn() {
824     return isSpeakerphoneOn;
825   }
826 
827   /**
828    * This method only handles app-level changes to the speakerphone. Audio layer changes should be
829    * handled separately. This is so that the VoicemailAudioManager can trigger changes to the
830    * presenter without the presenter triggering the audio manager and duplicating actions.
831    */
setSpeakerphoneOn(boolean on)832   public void setSpeakerphoneOn(boolean on) {
833     if (view == null) {
834       return;
835     }
836 
837     view.onSpeakerphoneOn(on);
838 
839     isSpeakerphoneOn = on;
840 
841     // This should run even if speakerphone is not being toggled because we may be switching
842     // from earpiece to headphone and vise versa. Also upon initial setup the default audio
843     // source is the earpiece, so we want to trigger the proximity sensor.
844     if (isPlaying) {
845       if (on || voicemailAudioManager.isWiredHeadsetPluggedIn()) {
846         disableProximitySensor(false /* waitForFarState */);
847       } else {
848         enableProximitySensor();
849       }
850     }
851   }
852 
853   @VisibleForTesting
clearInstance()854   public void clearInstance() {
855     instance = null;
856   }
857 
showShareVoicemailButton(boolean show)858   private void showShareVoicemailButton(boolean show) {
859     if (context == null) {
860       return;
861     }
862     if (isShareVoicemailAllowed(context) && shareVoicemailButtonView != null) {
863       if (show) {
864         Logger.get(context).logImpression(DialerImpression.Type.VVM_SHARE_VISIBLE);
865       }
866       LogUtil.d("VoicemailPlaybackPresenter.showShareVoicemailButton", "show: %b", show);
867       shareVoicemailButtonView.setVisibility(show ? View.VISIBLE : View.GONE);
868     }
869   }
870 
isShareVoicemailAllowed(Context context)871   private static boolean isShareVoicemailAllowed(Context context) {
872     return ConfigProviderComponent.get(context)
873         .getConfigProvider()
874         .getBoolean(CONFIG_SHARE_VOICEMAIL_ALLOWED, true);
875   }
876 
877   private static class ShareVoicemailWorker
878       implements DialerExecutor.Worker<Pair<Context, Uri>, Pair<Uri, String>> {
879 
880     @Nullable
881     @Override
doInBackground(Pair<Context, Uri> input)882     public Pair<Uri, String> doInBackground(Pair<Context, Uri> input) {
883       Context context = input.first;
884       Uri voicemailUri = input.second;
885       ContentResolver contentResolver = context.getContentResolver();
886       try (Cursor callLogInfo = getCallLogInfoCursor(contentResolver, voicemailUri);
887           Cursor contentInfo = getContentInfoCursor(contentResolver, voicemailUri)) {
888 
889         if (hasContent(callLogInfo) && hasContent(contentInfo)) {
890           String cachedName = callLogInfo.getString(CallLogQuery.CACHED_NAME);
891           String number = contentInfo.getString(contentInfo.getColumnIndex(Voicemails.NUMBER));
892           long date = contentInfo.getLong(contentInfo.getColumnIndex(Voicemails.DATE));
893           String mimeType = contentInfo.getString(contentInfo.getColumnIndex(Voicemails.MIME_TYPE));
894           String transcription =
895               contentInfo.getString(contentInfo.getColumnIndex(Voicemails.TRANSCRIPTION));
896 
897           // Copy voicemail content to a new file.
898           // Please see reference in third_party/java_src/android_app/dialer/java/com/android/
899           // dialer/app/res/xml/file_paths.xml for correct cache directory name.
900           File parentDir = new File(context.getCacheDir(), "my_cache");
901           if (!parentDir.exists()) {
902             parentDir.mkdirs();
903           }
904           File temporaryVoicemailFile =
905               new File(parentDir, getFileName(cachedName, number, mimeType, date));
906 
907           try (InputStream inputStream = contentResolver.openInputStream(voicemailUri);
908               OutputStream outputStream =
909                   contentResolver.openOutputStream(Uri.fromFile(temporaryVoicemailFile))) {
910             if (inputStream != null && outputStream != null) {
911               ByteStreams.copy(inputStream, outputStream);
912               return new Pair<>(
913                   FileProvider.getUriForFile(
914                       context, Constants.get().getFileProviderAuthority(), temporaryVoicemailFile),
915                   transcription);
916             }
917           } catch (IOException e) {
918             LogUtil.e(
919                 "VoicemailAsyncTaskUtil.shareVoicemail",
920                 "failed to copy voicemail content to new file: ",
921                 e);
922           }
923           return null;
924         }
925       }
926       return null;
927     }
928   }
929 
930   /**
931    * Share voicemail to be opened by user selected apps. This method will collect information, copy
932    * voicemail to a temporary file in background and launch a chooser intent to share it.
933    */
shareVoicemail()934   public void shareVoicemail() {
935     shareVoicemailExecutor.executeParallel(new Pair<>(context, voicemailUri));
936   }
937 
getFileName(String cachedName, String number, String mimeType, long date)938   private static String getFileName(String cachedName, String number, String mimeType, long date) {
939     String callerName = TextUtils.isEmpty(cachedName) ? number : cachedName;
940     SimpleDateFormat simpleDateFormat =
941         new SimpleDateFormat(VOICEMAIL_SHARE_FILE_NAME_DATE_FORMAT, Locale.getDefault());
942 
943     String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
944 
945     return callerName
946         + "_"
947         + simpleDateFormat.format(new Date(date))
948         + (TextUtils.isEmpty(fileExtension) ? "" : "." + fileExtension);
949   }
950 
getShareIntent( Context context, Uri voicemailFileUri, String transcription)951   private static Intent getShareIntent(
952       Context context, Uri voicemailFileUri, String transcription) {
953     Intent shareIntent = new Intent();
954     if (TextUtils.isEmpty(transcription)) {
955       shareIntent.setAction(Intent.ACTION_SEND);
956       shareIntent.putExtra(Intent.EXTRA_STREAM, voicemailFileUri);
957       shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
958       shareIntent.setType(context.getContentResolver().getType(voicemailFileUri));
959     } else {
960       shareIntent.setAction(Intent.ACTION_SEND);
961       shareIntent.putExtra(Intent.EXTRA_STREAM, voicemailFileUri);
962       shareIntent.putExtra(Intent.EXTRA_TEXT, transcription);
963       shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
964       shareIntent.setType(context.getContentResolver().getType(voicemailFileUri));
965     }
966 
967     return shareIntent;
968   }
969 
hasContent(@ullable Cursor cursor)970   private static boolean hasContent(@Nullable Cursor cursor) {
971     return cursor != null && cursor.moveToFirst();
972   }
973 
974   @Nullable
getCallLogInfoCursor(ContentResolver contentResolver, Uri voicemailUri)975   private static Cursor getCallLogInfoCursor(ContentResolver contentResolver, Uri voicemailUri) {
976     return contentResolver.query(
977         ContentUris.withAppendedId(
978             CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL, ContentUris.parseId(voicemailUri)),
979         CallLogQuery.getProjection(),
980         null,
981         null,
982         null);
983   }
984 
985   @Nullable
getContentInfoCursor(ContentResolver contentResolver, Uri voicemailUri)986   private static Cursor getContentInfoCursor(ContentResolver contentResolver, Uri voicemailUri) {
987     return contentResolver.query(
988         voicemailUri,
989         new String[] {
990           Voicemails._ID,
991           Voicemails.NUMBER,
992           Voicemails.DATE,
993           Voicemails.MIME_TYPE,
994           Voicemails.TRANSCRIPTION,
995         },
996         null,
997         null,
998         null);
999   }
1000 
1001   /** The enumeration of {@link AsyncTask} objects we use in this class. */
1002   public enum Tasks {
1003     CHECK_FOR_CONTENT,
1004     CHECK_CONTENT_AFTER_CHANGE,
1005     SHARE_VOICEMAIL,
1006     SEND_FETCH_REQUEST
1007   }
1008 
1009   /** Contract describing the behaviour we need from the ui we are controlling. */
1010   public interface PlaybackView {
1011 
getDesiredClipPosition()1012     int getDesiredClipPosition();
1013 
disableUiElements()1014     void disableUiElements();
1015 
enableUiElements()1016     void enableUiElements();
1017 
onPlaybackError()1018     void onPlaybackError();
1019 
onPlaybackStarted(int duration, ScheduledExecutorService executorService)1020     void onPlaybackStarted(int duration, ScheduledExecutorService executorService);
1021 
onPlaybackStopped()1022     void onPlaybackStopped();
1023 
onSpeakerphoneOn(boolean on)1024     void onSpeakerphoneOn(boolean on);
1025 
setClipPosition(int clipPositionInMillis, int clipLengthInMillis)1026     void setClipPosition(int clipPositionInMillis, int clipLengthInMillis);
1027 
setSuccess()1028     void setSuccess();
1029 
setFetchContentTimeout()1030     void setFetchContentTimeout();
1031 
setIsFetchingContent()1032     void setIsFetchingContent();
1033 
setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri)1034     void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri);
1035 
resetSeekBar()1036     void resetSeekBar();
1037   }
1038 
1039   public interface OnVoicemailDeletedListener {
1040 
onVoicemailDeleted(CallLogListItemViewHolder viewHolder, Uri uri)1041     void onVoicemailDeleted(CallLogListItemViewHolder viewHolder, Uri uri);
1042 
onVoicemailDeleteUndo(long rowId, int adaptorPosition, Uri uri)1043     void onVoicemailDeleteUndo(long rowId, int adaptorPosition, Uri uri);
1044 
onVoicemailDeletedInDatabase(long rowId, Uri uri)1045     void onVoicemailDeletedInDatabase(long rowId, Uri uri);
1046   }
1047 
1048   protected interface OnContentCheckedListener {
1049 
onContentChecked(boolean hasContent)1050     void onContentChecked(boolean hasContent);
1051   }
1052 
1053   @ThreadSafe
1054   private class FetchResultHandler extends ContentObserver implements Runnable {
1055 
1056     private final Handler fetchResultHandler;
1057     private final Uri voicemailUri;
1058     private AtomicBoolean isWaitingForResult = new AtomicBoolean(true);
1059 
FetchResultHandler(Handler handler, Uri uri, int code)1060     public FetchResultHandler(Handler handler, Uri uri, int code) {
1061       super(handler);
1062       fetchResultHandler = handler;
1063       voicemailUri = uri;
1064       if (context != null) {
1065         if (PermissionsUtil.hasReadVoicemailPermissions(context)) {
1066           context.getContentResolver().registerContentObserver(voicemailUri, false, this);
1067         }
1068         fetchResultHandler.postDelayed(this, FETCH_CONTENT_TIMEOUT_MS);
1069       }
1070     }
1071 
1072     /** Stop waiting for content and notify UI if {@link FETCH_CONTENT_TIMEOUT_MS} has elapsed. */
1073     @Override
run()1074     public void run() {
1075       if (isWaitingForResult.getAndSet(false) && context != null) {
1076         context.getContentResolver().unregisterContentObserver(this);
1077         if (view != null) {
1078           view.setFetchContentTimeout();
1079         }
1080       }
1081     }
1082 
destroy()1083     public void destroy() {
1084       if (isWaitingForResult.getAndSet(false) && context != null) {
1085         context.getContentResolver().unregisterContentObserver(this);
1086         fetchResultHandler.removeCallbacks(this);
1087       }
1088     }
1089 
1090     @Override
onChange(boolean selfChange)1091     public void onChange(boolean selfChange) {
1092       asyncTaskExecutor.submit(
1093           Tasks.CHECK_CONTENT_AFTER_CHANGE,
1094           new AsyncTask<Void, Void, Boolean>() {
1095 
1096             @Override
1097             public Boolean doInBackground(Void... params) {
1098               return queryHasContent(voicemailUri);
1099             }
1100 
1101             @Override
1102             public void onPostExecute(Boolean hasContent) {
1103               if (hasContent && context != null && isWaitingForResult.getAndSet(false)) {
1104                 context.getContentResolver().unregisterContentObserver(FetchResultHandler.this);
1105                 showShareVoicemailButton(true);
1106                 prepareContent();
1107               }
1108             }
1109           });
1110     }
1111   }
1112 }
1113