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 mContext;
     62   private CallLogListItemViewHolder mViewHolder;
     63   private VoicemailPlaybackPresenter mPresenter;
     64   /** Click listener to toggle speakerphone. */
     65   private final View.OnClickListener mSpeakerphoneListener =
     66       new View.OnClickListener() {
     67         @Override
     68         public void onClick(View v) {
     69           if (mPresenter != null) {
     70             mPresenter.toggleSpeakerphone();
     71           }
     72         }
     73       };
     74 
     75   private Uri mVoicemailUri;
     76   private final View.OnClickListener mDeleteButtonListener =
     77       new View.OnClickListener() {
     78         @Override
     79         public void onClick(View view) {
     80           Logger.get(mContext).logImpression(DialerImpression.Type.VOICEMAIL_DELETE_ENTRY);
     81           if (mPresenter == 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: refactor this so the view holder will always be valid.
     89           final int adapterPosition = mViewHolder.getAdapterPosition();
     90 
     91           mPresenter.pausePlayback();
     92           mPresenter.onVoicemailDeleted(mViewHolder);
     93 
     94           final Uri deleteUri = mVoicemailUri;
     95           final Runnable deleteCallback =
     96               new Runnable() {
     97                 @Override
     98                 public void run() {
     99                   if (Objects.equals(deleteUri, mVoicemailUri)) {
    100                     CallLogAsyncTaskUtil.deleteVoicemail(
    101                         mContext, 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                       mPresenter.onVoicemailDeleteUndo(adapterPosition);
    122                       handler.removeCallbacks(deleteCallback);
    123                     }
    124                   })
    125               .setActionTextColor(
    126                   mContext.getResources().getColor(R.color.dialer_snackbar_action_text_color))
    127               .show();
    128         }
    129       };
    130   private boolean mIsPlaying = false;
    131   /** Click listener to play or pause voicemail playback. */
    132   private final View.OnClickListener mStartStopButtonListener =
    133       new View.OnClickListener() {
    134         @Override
    135         public void onClick(View view) {
    136           if (mPresenter == null) {
    137             return;
    138           }
    139 
    140           if (mIsPlaying) {
    141             mPresenter.pausePlayback();
    142           } else {
    143             Logger.get(mContext)
    144                 .logImpression(DialerImpression.Type.VOICEMAIL_PLAY_AUDIO_AFTER_EXPANDING_ENTRY);
    145             mPresenter.resumePlayback();
    146           }
    147         }
    148       };
    149 
    150   private SeekBar mPlaybackSeek;
    151   private ImageButton mStartStopButton;
    152   private ImageButton mPlaybackSpeakerphone;
    153   private ImageButton mDeleteButton;
    154   private TextView mStateText;
    155   private TextView mPositionText;
    156   private TextView mTotalDurationText;
    157   /** Handle state changes when the user manipulates the seek bar. */
    158   private final OnSeekBarChangeListener mSeekBarChangeListener =
    159       new OnSeekBarChangeListener() {
    160         @Override
    161         public void onStartTrackingTouch(SeekBar seekBar) {
    162           if (mPresenter != null) {
    163             mPresenter.pausePlaybackForSeeking();
    164           }
    165         }
    166 
    167         @Override
    168         public void onStopTrackingTouch(SeekBar seekBar) {
    169           if (mPresenter != null) {
    170             mPresenter.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             mPresenter.seek(progress);
    181           }
    182         }
    183       };
    184 
    185   private PositionUpdater mPositionUpdater;
    186   private Drawable mVoicemailSeekHandleEnabled;
    187   private Drawable mVoicemailSeekHandleDisabled;
    188 
    189   public VoicemailPlaybackLayout(Context context) {
    190     this(context, null);
    191   }
    192 
    193   public VoicemailPlaybackLayout(Context context, AttributeSet attrs) {
    194     super(context, attrs);
    195     mContext = 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.mViewHolder = mViewHolder;
    203   }
    204 
    205   @Override
    206   public void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri) {
    207     mPresenter = presenter;
    208     mVoicemailUri = voicemailUri;
    209   }
    210 
    211   @Override
    212   protected void onFinishInflate() {
    213     super.onFinishInflate();
    214 
    215     mPlaybackSeek = (SeekBar) findViewById(R.id.playback_seek);
    216     mStartStopButton = (ImageButton) findViewById(R.id.playback_start_stop);
    217     mPlaybackSpeakerphone = (ImageButton) findViewById(R.id.playback_speakerphone);
    218     mDeleteButton = (ImageButton) findViewById(R.id.delete_voicemail);
    219 
    220     mStateText = (TextView) findViewById(R.id.playback_state_text);
    221     mStateText.setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
    222     mPositionText = (TextView) findViewById(R.id.playback_position_text);
    223     mTotalDurationText = (TextView) findViewById(R.id.total_duration_text);
    224 
    225     mPlaybackSeek.setOnSeekBarChangeListener(mSeekBarChangeListener);
    226     mStartStopButton.setOnClickListener(mStartStopButtonListener);
    227     mPlaybackSpeakerphone.setOnClickListener(mSpeakerphoneListener);
    228     mDeleteButton.setOnClickListener(mDeleteButtonListener);
    229 
    230     mPositionText.setText(formatAsMinutesAndSeconds(0));
    231     mTotalDurationText.setText(formatAsMinutesAndSeconds(0));
    232 
    233     mVoicemailSeekHandleEnabled =
    234         getResources().getDrawable(R.drawable.ic_voicemail_seek_handle, mContext.getTheme());
    235     mVoicemailSeekHandleDisabled =
    236         getResources()
    237             .getDrawable(R.drawable.ic_voicemail_seek_handle_disabled, mContext.getTheme());
    238   }
    239 
    240   @Override
    241   public void onPlaybackStarted(int duration, ScheduledExecutorService executorService) {
    242     mIsPlaying = true;
    243 
    244     mStartStopButton.setImageResource(R.drawable.ic_pause);
    245 
    246     if (mPositionUpdater != null) {
    247       mPositionUpdater.stopUpdating();
    248       mPositionUpdater = null;
    249     }
    250     mPositionUpdater = new PositionUpdater(duration, executorService);
    251     mPositionUpdater.startUpdating();
    252   }
    253 
    254   @Override
    255   public void onPlaybackStopped() {
    256     mIsPlaying = false;
    257 
    258     mStartStopButton.setImageResource(R.drawable.ic_play_arrow);
    259 
    260     if (mPositionUpdater != null) {
    261       mPositionUpdater.stopUpdating();
    262       mPositionUpdater = null;
    263     }
    264   }
    265 
    266   @Override
    267   public void onPlaybackError() {
    268     if (mPositionUpdater != null) {
    269       mPositionUpdater.stopUpdating();
    270     }
    271 
    272     disableUiElements();
    273     mStateText.setText(getString(R.string.voicemail_playback_error));
    274   }
    275 
    276   @Override
    277   public void onSpeakerphoneOn(boolean on) {
    278     if (on) {
    279       mPlaybackSpeakerphone.setImageResource(R.drawable.quantum_ic_volume_up_white_24);
    280       // Speaker is now on, tapping button will turn it off.
    281       mPlaybackSpeakerphone.setContentDescription(
    282           mContext.getString(R.string.voicemail_speaker_off));
    283     } else {
    284       mPlaybackSpeakerphone.setImageResource(R.drawable.quantum_ic_volume_down_white_24);
    285       // Speaker is now off, tapping button will turn it on.
    286       mPlaybackSpeakerphone.setContentDescription(
    287           mContext.getString(R.string.voicemail_speaker_on));
    288     }
    289   }
    290 
    291   @Override
    292   public void setClipPosition(int positionMs, int durationMs) {
    293     int seekBarPositionMs = Math.max(0, positionMs);
    294     int seekBarMax = Math.max(seekBarPositionMs, durationMs);
    295     if (mPlaybackSeek.getMax() != seekBarMax) {
    296       mPlaybackSeek.setMax(seekBarMax);
    297     }
    298 
    299     mPlaybackSeek.setProgress(seekBarPositionMs);
    300 
    301     mPositionText.setText(formatAsMinutesAndSeconds(seekBarPositionMs));
    302     mTotalDurationText.setText(formatAsMinutesAndSeconds(durationMs));
    303   }
    304 
    305   @Override
    306   public void setSuccess() {
    307     mStateText.setText(null);
    308   }
    309 
    310   @Override
    311   public void setIsFetchingContent() {
    312     disableUiElements();
    313     mStateText.setText(getString(R.string.voicemail_fetching_content));
    314   }
    315 
    316   @Override
    317   public void setFetchContentTimeout() {
    318     mStartStopButton.setEnabled(true);
    319     mStateText.setText(getString(R.string.voicemail_fetching_timout));
    320   }
    321 
    322   @Override
    323   public int getDesiredClipPosition() {
    324     return mPlaybackSeek.getProgress();
    325   }
    326 
    327   @Override
    328   public void disableUiElements() {
    329     mStartStopButton.setEnabled(false);
    330     resetSeekBar();
    331   }
    332 
    333   @Override
    334   public void enableUiElements() {
    335     mDeleteButton.setEnabled(true);
    336     mStartStopButton.setEnabled(true);
    337     mPlaybackSeek.setEnabled(true);
    338     mPlaybackSeek.setThumb(mVoicemailSeekHandleEnabled);
    339   }
    340 
    341   @Override
    342   public void resetSeekBar() {
    343     mPlaybackSeek.setProgress(0);
    344     mPlaybackSeek.setEnabled(false);
    345     mPlaybackSeek.setThumb(mVoicemailSeekHandleDisabled);
    346   }
    347 
    348   @Override
    349   public void onDeleteVoicemail() {
    350     mPresenter.onVoicemailDeletedInDatabase();
    351   }
    352 
    353   private String getString(int resId) {
    354     return mContext.getString(resId);
    355   }
    356 
    357   /**
    358    * Formats a number of milliseconds as something that looks like {@code 00:05}.
    359    *
    360    * <p>We always use four digits, two for minutes two for seconds. In the very unlikely event that
    361    * the voicemail duration exceeds 99 minutes, the display is capped at 99 minutes.
    362    */
    363   private String formatAsMinutesAndSeconds(int millis) {
    364     int seconds = millis / 1000;
    365     int minutes = seconds / 60;
    366     seconds -= minutes * 60;
    367     if (minutes > 99) {
    368       minutes = 99;
    369     }
    370     return String.format("%02d:%02d", minutes, seconds);
    371   }
    372 
    373   @VisibleForTesting
    374   public String getStateText() {
    375     return mStateText.getText().toString();
    376   }
    377 
    378   /** Controls the animation of the playback slider. */
    379   @ThreadSafe
    380   private final class PositionUpdater implements Runnable {
    381 
    382     /** Update rate for the slider, 30fps. */
    383     private static final int SLIDER_UPDATE_PERIOD_MILLIS = 1000 / 30;
    384 
    385     private final ScheduledExecutorService mExecutorService;
    386     private final Object mLock = new Object();
    387     private int mDurationMs;
    388 
    389     @GuardedBy("mLock")
    390     private ScheduledFuture<?> mScheduledFuture;
    391 
    392     private Runnable mUpdateClipPositionRunnable =
    393         new Runnable() {
    394           @Override
    395           public void run() {
    396             int currentPositionMs = 0;
    397             synchronized (mLock) {
    398               if (mScheduledFuture == null || mPresenter == null) {
    399                 // This task has been canceled. Just stop now.
    400                 return;
    401               }
    402               currentPositionMs = mPresenter.getMediaPlayerPosition();
    403             }
    404             setClipPosition(currentPositionMs, mDurationMs);
    405           }
    406         };
    407 
    408     public PositionUpdater(int durationMs, ScheduledExecutorService executorService) {
    409       mDurationMs = durationMs;
    410       mExecutorService = executorService;
    411     }
    412 
    413     @Override
    414     public void run() {
    415       post(mUpdateClipPositionRunnable);
    416     }
    417 
    418     public void startUpdating() {
    419       synchronized (mLock) {
    420         cancelPendingRunnables();
    421         mScheduledFuture =
    422             mExecutorService.scheduleAtFixedRate(
    423                 this, 0, SLIDER_UPDATE_PERIOD_MILLIS, TimeUnit.MILLISECONDS);
    424       }
    425     }
    426 
    427     public void stopUpdating() {
    428       synchronized (mLock) {
    429         cancelPendingRunnables();
    430       }
    431     }
    432 
    433     @GuardedBy("mLock")
    434     private void cancelPendingRunnables() {
    435       if (mScheduledFuture != null) {
    436         mScheduledFuture.cancel(true);
    437         mScheduledFuture = null;
    438       }
    439       removeCallbacks(mUpdateClipPositionRunnable);
    440     }
    441   }
    442 }
    443