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.content.Context;
     20 import android.graphics.drawable.Drawable;
     21 import android.net.Uri;
     22 import android.os.Handler;
     23 import android.support.annotation.VisibleForTesting;
     24 import android.support.design.widget.Snackbar;
     25 import android.util.AttributeSet;
     26 import android.view.LayoutInflater;
     27 import android.view.View;
     28 import android.widget.ImageButton;
     29 import android.widget.LinearLayout;
     30 import android.widget.SeekBar;
     31 import android.widget.SeekBar.OnSeekBarChangeListener;
     32 import android.widget.TextView;
     33 import com.android.dialer.app.R;
     34 import com.android.dialer.app.calllog.CallLogAsyncTaskUtil;
     35 import com.android.dialer.app.calllog.CallLogListItemViewHolder;
     36 import com.android.dialer.logging.DialerImpression;
     37 import com.android.dialer.logging.Logger;
     38 import java.util.Objects;
     39 import java.util.concurrent.ScheduledExecutorService;
     40 import java.util.concurrent.ScheduledFuture;
     41 import java.util.concurrent.TimeUnit;
     42 import javax.annotation.concurrent.GuardedBy;
     43 import javax.annotation.concurrent.NotThreadSafe;
     44 import javax.annotation.concurrent.ThreadSafe;
     45 
     46 /**
     47  * Displays and plays a single voicemail. See {@link VoicemailPlaybackPresenter} for details on the
     48  * voicemail playback implementation.
     49  *
     50  * <p>This class is not thread-safe, it is thread-confined. All calls to all public methods on this
     51  * class are expected to come from the main ui thread.
     52  */
     53 @NotThreadSafe
     54 public class VoicemailPlaybackLayout extends LinearLayout
     55     implements VoicemailPlaybackPresenter.PlaybackView,
     56         CallLogAsyncTaskUtil.CallLogAsyncTaskListener {
     57 
     58   private static final String TAG = VoicemailPlaybackLayout.class.getSimpleName();
     59   private static final int VOICEMAIL_DELETE_DELAY_MS = 3000;
     60 
     61   private Context context;
     62   private CallLogListItemViewHolder viewHolder;
     63   private VoicemailPlaybackPresenter presenter;
     64   /** Click listener to toggle speakerphone. */
     65   private final View.OnClickListener speakerphoneListener =
     66       new View.OnClickListener() {
     67         @Override
     68         public void onClick(View v) {
     69           if (presenter != null) {
     70             presenter.toggleSpeakerphone();
     71           }
     72         }
     73       };
     74 
     75   private Uri voicemailUri;
     76   private final View.OnClickListener deleteButtonListener =
     77       new View.OnClickListener() {
     78         @Override
     79         public void onClick(View view) {
     80           Logger.get(context).logImpression(DialerImpression.Type.VOICEMAIL_DELETE_ENTRY);
     81           if (presenter == null) {
     82             return;
     83           }
     84 
     85           // When the undo button is pressed, the viewHolder we have is no longer valid because when
     86           // we hide the view it is binded to something else, and the layout is not updated for
     87           // hidden items. copy the adapter position so we can update the view upon undo.
     88           // TODO(twyen): refactor this so the view holder will always be valid.
     89           final int adapterPosition = viewHolder.getAdapterPosition();
     90 
     91           presenter.pausePlayback();
     92           presenter.onVoicemailDeleted(viewHolder);
     93 
     94           final Uri deleteUri = voicemailUri;
     95           final Runnable deleteCallback =
     96               new Runnable() {
     97                 @Override
     98                 public void run() {
     99                   if (Objects.equals(deleteUri, voicemailUri)) {
    100                     CallLogAsyncTaskUtil.deleteVoicemail(
    101                         context, deleteUri, VoicemailPlaybackLayout.this);
    102                   }
    103                 }
    104               };
    105 
    106           final Handler handler = new Handler();
    107           // Add a little buffer time in case the user clicked "undo" at the end of the delay
    108           // window.
    109           handler.postDelayed(deleteCallback, VOICEMAIL_DELETE_DELAY_MS + 50);
    110 
    111           Snackbar.make(
    112                   VoicemailPlaybackLayout.this,
    113                   R.string.snackbar_voicemail_deleted,
    114                   Snackbar.LENGTH_LONG)
    115               .setDuration(VOICEMAIL_DELETE_DELAY_MS)
    116               .setAction(
    117                   R.string.snackbar_voicemail_deleted_undo,
    118                   new View.OnClickListener() {
    119                     @Override
    120                     public void onClick(View view) {
    121                       presenter.onVoicemailDeleteUndo(adapterPosition);
    122                       handler.removeCallbacks(deleteCallback);
    123                     }
    124                   })
    125               .setActionTextColor(
    126                   context.getResources().getColor(R.color.dialer_snackbar_action_text_color))
    127               .show();
    128         }
    129       };
    130   private boolean isPlaying = false;
    131   /** Click listener to play or pause voicemail playback. */
    132   private final View.OnClickListener startStopButtonListener =
    133       new View.OnClickListener() {
    134         @Override
    135         public void onClick(View view) {
    136           if (presenter == null) {
    137             return;
    138           }
    139 
    140           if (isPlaying) {
    141             presenter.pausePlayback();
    142           } else {
    143             Logger.get(context)
    144                 .logImpression(DialerImpression.Type.VOICEMAIL_PLAY_AUDIO_AFTER_EXPANDING_ENTRY);
    145             presenter.resumePlayback();
    146           }
    147         }
    148       };
    149 
    150   private SeekBar playbackSeek;
    151   private ImageButton startStopButton;
    152   private ImageButton playbackSpeakerphone;
    153   private ImageButton deleteButton;
    154   private TextView stateText;
    155   private TextView positionText;
    156   private TextView totalDurationText;
    157   /** Handle state changes when the user manipulates the seek bar. */
    158   private final OnSeekBarChangeListener seekBarChangeListener =
    159       new OnSeekBarChangeListener() {
    160         @Override
    161         public void onStartTrackingTouch(SeekBar seekBar) {
    162           if (presenter != null) {
    163             presenter.pausePlaybackForSeeking();
    164           }
    165         }
    166 
    167         @Override
    168         public void onStopTrackingTouch(SeekBar seekBar) {
    169           if (presenter != null) {
    170             presenter.resumePlaybackAfterSeeking(seekBar.getProgress());
    171           }
    172         }
    173 
    174         @Override
    175         public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    176           setClipPosition(progress, seekBar.getMax());
    177           // Update the seek position if user manually changed it. This makes sure position gets
    178           // updated when user use volume button to seek playback in talkback mode.
    179           if (fromUser) {
    180             presenter.seek(progress);
    181           }
    182         }
    183       };
    184 
    185   private PositionUpdater positionUpdater;
    186   private Drawable voicemailSeekHandleEnabled;
    187   private Drawable voicemailSeekHandleDisabled;
    188 
    189   public VoicemailPlaybackLayout(Context context) {
    190     this(context, null);
    191   }
    192 
    193   public VoicemailPlaybackLayout(Context context, AttributeSet attrs) {
    194     super(context, attrs);
    195     this.context = context;
    196     LayoutInflater inflater =
    197         (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    198     inflater.inflate(R.layout.voicemail_playback_layout, this);
    199   }
    200 
    201   public void setViewHolder(CallLogListItemViewHolder mViewHolder) {
    202     this.viewHolder = mViewHolder;
    203   }
    204 
    205   @Override
    206   public void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri) {
    207     this.presenter = presenter;
    208     this.voicemailUri = voicemailUri;
    209   }
    210 
    211   @Override
    212   protected void onFinishInflate() {
    213     super.onFinishInflate();
    214 
    215     playbackSeek = (SeekBar) findViewById(R.id.playback_seek);
    216     startStopButton = (ImageButton) findViewById(R.id.playback_start_stop);
    217     playbackSpeakerphone = (ImageButton) findViewById(R.id.playback_speakerphone);
    218     deleteButton = (ImageButton) findViewById(R.id.delete_voicemail);
    219 
    220     stateText = (TextView) findViewById(R.id.playback_state_text);
    221     stateText.setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
    222     positionText = (TextView) findViewById(R.id.playback_position_text);
    223     totalDurationText = (TextView) findViewById(R.id.total_duration_text);
    224 
    225     playbackSeek.setOnSeekBarChangeListener(seekBarChangeListener);
    226     startStopButton.setOnClickListener(startStopButtonListener);
    227     playbackSpeakerphone.setOnClickListener(speakerphoneListener);
    228     deleteButton.setOnClickListener(deleteButtonListener);
    229 
    230     positionText.setText(formatAsMinutesAndSeconds(0));
    231     totalDurationText.setText(formatAsMinutesAndSeconds(0));
    232 
    233     voicemailSeekHandleEnabled =
    234         getResources().getDrawable(R.drawable.ic_voicemail_seek_handle, context.getTheme());
    235     voicemailSeekHandleDisabled =
    236         getResources()
    237             .getDrawable(R.drawable.ic_voicemail_seek_handle_disabled, context.getTheme());
    238   }
    239 
    240   @Override
    241   public void onPlaybackStarted(int duration, ScheduledExecutorService executorService) {
    242     isPlaying = true;
    243 
    244     startStopButton.setImageResource(R.drawable.ic_pause);
    245 
    246     if (positionUpdater != null) {
    247       positionUpdater.stopUpdating();
    248       positionUpdater = null;
    249     }
    250     positionUpdater = new PositionUpdater(duration, executorService);
    251     positionUpdater.startUpdating();
    252   }
    253 
    254   @Override
    255   public void onPlaybackStopped() {
    256     isPlaying = false;
    257 
    258     startStopButton.setImageResource(R.drawable.ic_play_arrow);
    259 
    260     if (positionUpdater != null) {
    261       positionUpdater.stopUpdating();
    262       positionUpdater = null;
    263     }
    264   }
    265 
    266   @Override
    267   public void onPlaybackError() {
    268     if (positionUpdater != null) {
    269       positionUpdater.stopUpdating();
    270     }
    271 
    272     disableUiElements();
    273     stateText.setText(getString(R.string.voicemail_playback_error));
    274   }
    275 
    276   @Override
    277   public void onSpeakerphoneOn(boolean on) {
    278     if (on) {
    279       playbackSpeakerphone.setImageResource(R.drawable.quantum_ic_volume_up_white_24);
    280       // Speaker is now on, tapping button will turn it off.
    281       playbackSpeakerphone.setContentDescription(context.getString(R.string.voicemail_speaker_off));
    282     } else {
    283       playbackSpeakerphone.setImageResource(R.drawable.quantum_ic_volume_down_white_24);
    284       // Speaker is now off, tapping button will turn it on.
    285       playbackSpeakerphone.setContentDescription(context.getString(R.string.voicemail_speaker_on));
    286     }
    287   }
    288 
    289   @Override
    290   public void setClipPosition(int positionMs, int durationMs) {
    291     int seekBarPositionMs = Math.max(0, positionMs);
    292     int seekBarMax = Math.max(seekBarPositionMs, durationMs);
    293     if (playbackSeek.getMax() != seekBarMax) {
    294       playbackSeek.setMax(seekBarMax);
    295     }
    296 
    297     playbackSeek.setProgress(seekBarPositionMs);
    298 
    299     positionText.setText(formatAsMinutesAndSeconds(seekBarPositionMs));
    300     totalDurationText.setText(formatAsMinutesAndSeconds(durationMs));
    301   }
    302 
    303   @Override
    304   public void setSuccess() {
    305     stateText.setText(null);
    306   }
    307 
    308   @Override
    309   public void setIsFetchingContent() {
    310     disableUiElements();
    311     stateText.setText(getString(R.string.voicemail_fetching_content));
    312   }
    313 
    314   @Override
    315   public void setFetchContentTimeout() {
    316     startStopButton.setEnabled(true);
    317     stateText.setText(getString(R.string.voicemail_fetching_timout));
    318   }
    319 
    320   @Override
    321   public int getDesiredClipPosition() {
    322     return playbackSeek.getProgress();
    323   }
    324 
    325   @Override
    326   public void disableUiElements() {
    327     startStopButton.setEnabled(false);
    328     resetSeekBar();
    329   }
    330 
    331   @Override
    332   public void enableUiElements() {
    333     deleteButton.setEnabled(true);
    334     startStopButton.setEnabled(true);
    335     playbackSeek.setEnabled(true);
    336     playbackSeek.setThumb(voicemailSeekHandleEnabled);
    337   }
    338 
    339   @Override
    340   public void resetSeekBar() {
    341     playbackSeek.setProgress(0);
    342     playbackSeek.setEnabled(false);
    343     playbackSeek.setThumb(voicemailSeekHandleDisabled);
    344   }
    345 
    346   @Override
    347   public void onDeleteVoicemail() {
    348     presenter.onVoicemailDeletedInDatabase();
    349   }
    350 
    351   private String getString(int resId) {
    352     return context.getString(resId);
    353   }
    354 
    355   /**
    356    * Formats a number of milliseconds as something that looks like {@code 00:05}.
    357    *
    358    * <p>We always use four digits, two for minutes two for seconds. In the very unlikely event that
    359    * the voicemail duration exceeds 99 minutes, the display is capped at 99 minutes.
    360    */
    361   private String formatAsMinutesAndSeconds(int millis) {
    362     int seconds = millis / 1000;
    363     int minutes = seconds / 60;
    364     seconds -= minutes * 60;
    365     if (minutes > 99) {
    366       minutes = 99;
    367     }
    368     return String.format("%02d:%02d", minutes, seconds);
    369   }
    370 
    371   @VisibleForTesting
    372   public String getStateText() {
    373     return stateText.getText().toString();
    374   }
    375 
    376   /** Controls the animation of the playback slider. */
    377   @ThreadSafe
    378   private final class PositionUpdater implements Runnable {
    379 
    380     /** Update rate for the slider, 30fps. */
    381     private static final int SLIDER_UPDATE_PERIOD_MILLIS = 1000 / 30;
    382 
    383     private final ScheduledExecutorService executorService;
    384     private final Object lock = new Object();
    385     private int durationMs;
    386 
    387     @GuardedBy("lock")
    388     private ScheduledFuture<?> scheduledFuture;
    389 
    390     private Runnable updateClipPositionRunnable =
    391         new Runnable() {
    392           @Override
    393           public void run() {
    394             int currentPositionMs = 0;
    395             synchronized (lock) {
    396               if (scheduledFuture == null || presenter == null) {
    397                 // This task has been canceled. Just stop now.
    398                 return;
    399               }
    400               currentPositionMs = presenter.getMediaPlayerPosition();
    401             }
    402             setClipPosition(currentPositionMs, durationMs);
    403           }
    404         };
    405 
    406     public PositionUpdater(int durationMs, ScheduledExecutorService executorService) {
    407       this.durationMs = durationMs;
    408       this.executorService = executorService;
    409     }
    410 
    411     @Override
    412     public void run() {
    413       post(updateClipPositionRunnable);
    414     }
    415 
    416     public void startUpdating() {
    417       synchronized (lock) {
    418         cancelPendingRunnables();
    419         scheduledFuture =
    420             executorService.scheduleAtFixedRate(
    421                 this, 0, SLIDER_UPDATE_PERIOD_MILLIS, TimeUnit.MILLISECONDS);
    422       }
    423     }
    424 
    425     public void stopUpdating() {
    426       synchronized (lock) {
    427         cancelPendingRunnables();
    428       }
    429     }
    430 
    431     @GuardedBy("lock")
    432     private void cancelPendingRunnables() {
    433       if (scheduledFuture != null) {
    434         scheduledFuture.cancel(true);
    435         scheduledFuture = null;
    436       }
    437       removeCallbacks(updateClipPositionRunnable);
    438     }
    439   }
    440 }
    441