Home | History | Annotate | Download | only in voicemail
      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. */
    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
    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 
    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
    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. */
    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. */
    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. */
    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    */
    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. */
    357   public void onResume() {
    358     voicemailAudioManager.registerReceivers();
    359   }
    360 
    361   /** Must be invoked when the parent activity is paused. */
    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. */
    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. */
    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 
    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    */
    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    */
    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 
    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
    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
    574   public boolean onError(MediaPlayer mp, int what, int extra) {
    575     handleError(new IllegalStateException("MediaPlayer error listener invoked: " + extra));
    576     return true;
    577   }
    578 
    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
    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    */
    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    */
    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. */
    690   public void pausePlayback() {
    691     pausePlayback(false);
    692   }
    693 
    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    */
    726   public void pausePlaybackForSeeking() {
    727     if (mediaPlayer != null) {
    728       shouldResumePlaybackAfterSeeking = mediaPlayer.isPlaying();
    729     }
    730     pausePlayback(true);
    731   }
    732 
    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    */
    745   public void seek(int position) {
    746     this.position = position;
    747     mediaPlayer.seekTo(this.position);
    748   }
    749 
    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 
    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. */
    787   public void toggleSpeakerphone() {
    788     voicemailAudioManager.setSpeakerphoneOn(!isSpeakerphoneOn);
    789     setSpeakerphoneOn(!isSpeakerphoneOn);
    790   }
    791 
    792   public void setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener) {
    793     onVoicemailDeletedListener = listener;
    794   }
    795 
    796   public int getMediaPlayerPosition() {
    797     return isPrepared && mediaPlayer != null ? mediaPlayer.getCurrentPosition() : 0;
    798   }
    799 
    800   void onVoicemailDeleted(CallLogListItemViewHolder viewHolder) {
    801     if (onVoicemailDeletedListener != null) {
    802       onVoicemailDeletedListener.onVoicemailDeleted(viewHolder, voicemailUri);
    803     }
    804   }
    805 
    806   void onVoicemailDeleteUndo(int adapterPosition) {
    807     if (onVoicemailDeletedListener != null) {
    808       onVoicemailDeletedListener.onVoicemailDeleteUndo(rowId, adapterPosition, voicemailUri);
    809     }
    810   }
    811 
    812   void onVoicemailDeletedInDatabase() {
    813     if (onVoicemailDeletedListener != null) {
    814       onVoicemailDeletedListener.onVoicemailDeletedInDatabase(rowId, voicemailUri);
    815     }
    816   }
    817 
    818   @VisibleForTesting
    819   public boolean isPlaying() {
    820     return isPlaying;
    821   }
    822 
    823   @VisibleForTesting
    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    */
    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
    855   public void clearInstance() {
    856     instance = null;
    857   }
    858 
    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 
    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
    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    */
    933   public void shareVoicemail() {
    934     shareVoicemailExecutor.executeParallel(new Pair<>(context, voicemailUri));
    935   }
    936 
    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 
    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 
    969   private static boolean hasContent(@Nullable Cursor cursor) {
    970     return cursor != null && cursor.moveToFirst();
    971   }
    972 
    973   @Nullable
    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
    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 
   1011     int getDesiredClipPosition();
   1012 
   1013     void disableUiElements();
   1014 
   1015     void enableUiElements();
   1016 
   1017     void onPlaybackError();
   1018 
   1019     void onPlaybackStarted(int duration, ScheduledExecutorService executorService);
   1020 
   1021     void onPlaybackStopped();
   1022 
   1023     void onSpeakerphoneOn(boolean on);
   1024 
   1025     void setClipPosition(int clipPositionInMillis, int clipLengthInMillis);
   1026 
   1027     void setSuccess();
   1028 
   1029     void setFetchContentTimeout();
   1030 
   1031     void setIsFetchingContent();
   1032 
   1033     void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri);
   1034 
   1035     void resetSeekBar();
   1036   }
   1037 
   1038   public interface OnVoicemailDeletedListener {
   1039 
   1040     void onVoicemailDeleted(CallLogListItemViewHolder viewHolder, Uri uri);
   1041 
   1042     void onVoicemailDeleteUndo(long rowId, int adaptorPosition, Uri uri);
   1043 
   1044     void onVoicemailDeletedInDatabase(long rowId, Uri uri);
   1045   }
   1046 
   1047   protected interface OnContentCheckedListener {
   1048 
   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 
   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
   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 
   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
   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