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.voicemail;
     18 
     19 import android.app.Activity;
     20 import android.app.Fragment;
     21 import android.content.Context;
     22 import android.media.MediaPlayer;
     23 import android.net.Uri;
     24 import android.os.Bundle;
     25 import android.os.PowerManager;
     26 import android.provider.VoicemailContract;
     27 import android.util.AttributeSet;
     28 import android.util.Log;
     29 import android.view.LayoutInflater;
     30 import android.view.View;
     31 import android.view.ViewGroup;
     32 import android.widget.ImageButton;
     33 import android.widget.LinearLayout;
     34 import android.widget.SeekBar;
     35 import android.widget.SeekBar.OnSeekBarChangeListener;
     36 import android.widget.TextView;
     37 
     38 import com.android.common.io.MoreCloseables;
     39 import com.android.dialer.R;
     40 import com.android.dialer.calllog.CallLogAsyncTaskUtil;
     41 
     42 import com.google.common.base.Preconditions;
     43 
     44 import java.util.concurrent.TimeUnit;
     45 import java.util.concurrent.ScheduledFuture;
     46 import java.util.concurrent.ScheduledExecutorService;
     47 
     48 import javax.annotation.concurrent.GuardedBy;
     49 import javax.annotation.concurrent.NotThreadSafe;
     50 import javax.annotation.concurrent.ThreadSafe;
     51 
     52 /**
     53  * Displays and plays a single voicemail. See {@link VoicemailPlaybackPresenter} for
     54  * details on the voicemail playback implementation.
     55  *
     56  * This class is not thread-safe, it is thread-confined. All calls to all public
     57  * methods on this class are expected to come from the main ui thread.
     58  */
     59 @NotThreadSafe
     60 public class VoicemailPlaybackLayout extends LinearLayout
     61         implements VoicemailPlaybackPresenter.PlaybackView {
     62     private static final String TAG = VoicemailPlaybackLayout.class.getSimpleName();
     63 
     64     /**
     65      * Controls the animation of the playback slider.
     66      */
     67     @ThreadSafe
     68     private final class PositionUpdater implements Runnable {
     69 
     70         /** Update rate for the slider, 30fps. */
     71         private static final int SLIDER_UPDATE_PERIOD_MILLIS = 1000 / 30;
     72 
     73         private int mDurationMs;
     74         private final ScheduledExecutorService mExecutorService;
     75         private final Object mLock = new Object();
     76         @GuardedBy("mLock") private ScheduledFuture<?> mScheduledFuture;
     77 
     78         private Runnable mUpdateClipPositionRunnable = new Runnable() {
     79             @Override
     80             public void run() {
     81                 int currentPositionMs = 0;
     82                 synchronized (mLock) {
     83                     if (mScheduledFuture == null || mPresenter == null) {
     84                         // This task has been canceled. Just stop now.
     85                         return;
     86                     }
     87                     currentPositionMs = mPresenter.getMediaPlayerPosition();
     88                 }
     89                 setClipPosition(currentPositionMs, mDurationMs);
     90             }
     91         };
     92 
     93         public PositionUpdater(int durationMs, ScheduledExecutorService executorService) {
     94             mDurationMs = durationMs;
     95             mExecutorService = executorService;
     96         }
     97 
     98         @Override
     99         public void run() {
    100             post(mUpdateClipPositionRunnable);
    101         }
    102 
    103         public void startUpdating() {
    104             synchronized (mLock) {
    105                 cancelPendingRunnables();
    106                 mScheduledFuture = mExecutorService.scheduleAtFixedRate(
    107                         this, 0, SLIDER_UPDATE_PERIOD_MILLIS, TimeUnit.MILLISECONDS);
    108             }
    109         }
    110 
    111         public void stopUpdating() {
    112             synchronized (mLock) {
    113                 cancelPendingRunnables();
    114             }
    115         }
    116 
    117         private void cancelPendingRunnables() {
    118             if (mScheduledFuture != null) {
    119                 mScheduledFuture.cancel(true);
    120                 mScheduledFuture = null;
    121             }
    122             removeCallbacks(mUpdateClipPositionRunnable);
    123         }
    124     }
    125 
    126     /**
    127      * Handle state changes when the user manipulates the seek bar.
    128      */
    129     private final OnSeekBarChangeListener mSeekBarChangeListener = new OnSeekBarChangeListener() {
    130         @Override
    131         public void onStartTrackingTouch(SeekBar seekBar) {
    132             if (mPresenter != null) {
    133                 mPresenter.pausePlaybackForSeeking();
    134             }
    135         }
    136 
    137         @Override
    138         public void onStopTrackingTouch(SeekBar seekBar) {
    139             if (mPresenter != null) {
    140                 mPresenter.resumePlaybackAfterSeeking(seekBar.getProgress());
    141             }
    142         }
    143 
    144         @Override
    145         public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    146             setClipPosition(progress, seekBar.getMax());
    147         }
    148     };
    149 
    150     /**
    151      * Click listener to toggle speakerphone.
    152      */
    153     private final View.OnClickListener mSpeakerphoneListener = new View.OnClickListener() {
    154         @Override
    155         public void onClick(View v) {
    156             if (mPresenter != null) {
    157                 onSpeakerphoneOn(!mPresenter.isSpeakerphoneOn());
    158             }
    159         }
    160     };
    161 
    162     /**
    163      * Click listener to play or pause voicemail playback.
    164      */
    165     private final View.OnClickListener mStartStopButtonListener = new View.OnClickListener() {
    166         @Override
    167         public void onClick(View view) {
    168             if (mPresenter == null) {
    169                 return;
    170             }
    171 
    172             if (mIsPlaying) {
    173                 mPresenter.pausePlayback();
    174             } else {
    175                 mPresenter.resumePlayback();
    176             }
    177         }
    178     };
    179 
    180     private final View.OnClickListener mDeleteButtonListener = new View.OnClickListener() {
    181         @Override
    182         public void onClick(View view ) {
    183             if (mPresenter == null) {
    184                 return;
    185             }
    186             mPresenter.pausePlayback();
    187             CallLogAsyncTaskUtil.deleteVoicemail(mContext, mVoicemailUri, null);
    188             mPresenter.onVoicemailDeleted();
    189         }
    190     };
    191 
    192     private Context mContext;
    193     private VoicemailPlaybackPresenter mPresenter;
    194     private Uri mVoicemailUri;
    195 
    196     private boolean mIsPlaying = false;
    197 
    198     private SeekBar mPlaybackSeek;
    199     private ImageButton mStartStopButton;
    200     private ImageButton mPlaybackSpeakerphone;
    201     private ImageButton mDeleteButton;
    202     private TextView mStateText;
    203     private TextView mPositionText;
    204     private TextView mTotalDurationText;
    205 
    206     private PositionUpdater mPositionUpdater;
    207 
    208     public VoicemailPlaybackLayout(Context context) {
    209         this(context, null);
    210     }
    211 
    212     public VoicemailPlaybackLayout(Context context, AttributeSet attrs) {
    213         super(context, attrs);
    214 
    215         mContext = context;
    216         LayoutInflater inflater =
    217                 (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    218         inflater.inflate(R.layout.voicemail_playback_layout, this);
    219     }
    220 
    221     @Override
    222     public void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri) {
    223         mPresenter = presenter;
    224         mVoicemailUri = voicemailUri;
    225     }
    226 
    227     @Override
    228     protected void onFinishInflate() {
    229         super.onFinishInflate();
    230 
    231         mPlaybackSeek = (SeekBar) findViewById(R.id.playback_seek);
    232         mStartStopButton = (ImageButton) findViewById(R.id.playback_start_stop);
    233         mPlaybackSpeakerphone = (ImageButton) findViewById(R.id.playback_speakerphone);
    234         mDeleteButton = (ImageButton) findViewById(R.id.delete_voicemail);
    235         mStateText = (TextView) findViewById(R.id.playback_state_text);
    236         mPositionText = (TextView) findViewById(R.id.playback_position_text);
    237         mTotalDurationText = (TextView) findViewById(R.id.total_duration_text);
    238 
    239         mPlaybackSeek.setOnSeekBarChangeListener(mSeekBarChangeListener);
    240         mStartStopButton.setOnClickListener(mStartStopButtonListener);
    241         mPlaybackSpeakerphone.setOnClickListener(mSpeakerphoneListener);
    242         mDeleteButton.setOnClickListener(mDeleteButtonListener);
    243     }
    244 
    245     @Override
    246     public void onPlaybackStarted(int duration, ScheduledExecutorService executorService) {
    247         mIsPlaying = true;
    248 
    249         mStartStopButton.setImageResource(R.drawable.ic_pause);
    250 
    251         if (mPresenter != null) {
    252             onSpeakerphoneOn(mPresenter.isSpeakerphoneOn());
    253         }
    254 
    255         if (mPositionUpdater != null) {
    256             mPositionUpdater.stopUpdating();
    257             mPositionUpdater = null;
    258         }
    259         mPositionUpdater = new PositionUpdater(duration, executorService);
    260         mPositionUpdater.startUpdating();
    261     }
    262 
    263     @Override
    264     public void onPlaybackStopped() {
    265         mIsPlaying = false;
    266 
    267         mStartStopButton.setImageResource(R.drawable.ic_play_arrow);
    268 
    269         if (mPositionUpdater != null) {
    270             mPositionUpdater.stopUpdating();
    271             mPositionUpdater = null;
    272         }
    273     }
    274 
    275     @Override
    276     public void onPlaybackError() {
    277         if (mPositionUpdater != null) {
    278             mPositionUpdater.stopUpdating();
    279         }
    280 
    281         disableUiElements();
    282         mStateText.setText(getString(R.string.voicemail_playback_error));
    283     }
    284 
    285 
    286     public void onSpeakerphoneOn(boolean on) {
    287         if (mPresenter != null) {
    288             mPresenter.setSpeakerphoneOn(on);
    289         }
    290 
    291         if (on) {
    292             mPlaybackSpeakerphone.setImageResource(R.drawable.ic_volume_up_24dp);
    293             // Speaker is now on, tapping button will turn it off.
    294             mPlaybackSpeakerphone.setContentDescription(
    295                     mContext.getString(R.string.voicemail_speaker_off));
    296         } else {
    297             mPlaybackSpeakerphone.setImageResource(R.drawable.ic_volume_down_24dp);
    298             // Speaker is now off, tapping button will turn it on.
    299             mPlaybackSpeakerphone.setContentDescription(
    300                     mContext.getString(R.string.voicemail_speaker_on));
    301         }
    302     }
    303 
    304     @Override
    305     public void setClipPosition(int positionMs, int durationMs) {
    306         int seekBarPositionMs = Math.max(0, positionMs);
    307         int seekBarMax = Math.max(seekBarPositionMs, durationMs);
    308         if (mPlaybackSeek.getMax() != seekBarMax) {
    309             mPlaybackSeek.setMax(seekBarMax);
    310         }
    311 
    312         mPlaybackSeek.setProgress(seekBarPositionMs);
    313 
    314         mPositionText.setText(formatAsMinutesAndSeconds(seekBarPositionMs));
    315         mTotalDurationText.setText(formatAsMinutesAndSeconds(durationMs));
    316         mStateText.setText(null);
    317     }
    318 
    319     @Override
    320     public void setIsBuffering() {
    321         disableUiElements();
    322         mStateText.setText(getString(R.string.voicemail_buffering));
    323     }
    324 
    325     @Override
    326     public void setIsFetchingContent() {
    327         disableUiElements();
    328         mStateText.setText(getString(R.string.voicemail_fetching_content));
    329     }
    330 
    331     @Override
    332     public void setFetchContentTimeout() {
    333         disableUiElements();
    334         mStateText.setText(getString(R.string.voicemail_fetching_timout));
    335     }
    336 
    337     @Override
    338     public int getDesiredClipPosition() {
    339         return mPlaybackSeek.getProgress();
    340     }
    341 
    342     @Override
    343     public void disableUiElements() {
    344         mStartStopButton.setEnabled(false);
    345         mPlaybackSpeakerphone.setEnabled(false);
    346         mPlaybackSeek.setProgress(0);
    347         mPlaybackSeek.setEnabled(false);
    348 
    349         mPositionText.setText(formatAsMinutesAndSeconds(0));
    350         mTotalDurationText.setText(formatAsMinutesAndSeconds(0));
    351     }
    352 
    353     @Override
    354     public void enableUiElements() {
    355         mStartStopButton.setEnabled(true);
    356         mPlaybackSpeakerphone.setEnabled(true);
    357         mPlaybackSeek.setEnabled(true);
    358     }
    359 
    360     private String getString(int resId) {
    361         return mContext.getString(resId);
    362     }
    363 
    364     /**
    365      * Formats a number of milliseconds as something that looks like {@code 00:05}.
    366      * <p>
    367      * We always use four digits, two for minutes two for seconds.  In the very unlikely event
    368      * that the voicemail duration exceeds 99 minutes, the display is capped at 99 minutes.
    369      */
    370     private String formatAsMinutesAndSeconds(int millis) {
    371         int seconds = millis / 1000;
    372         int minutes = seconds / 60;
    373         seconds -= minutes * 60;
    374         if (minutes > 99) {
    375             minutes = 99;
    376         }
    377         return String.format("%02d:%02d", minutes, seconds);
    378     }
    379 }
    380