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