441 lines
14 KiB
Java
441 lines
14 KiB
Java
|
/*
|
||
|
* Copyright (C) 2011 The Android Open Source Project
|
||
|
*
|
||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
* you may not use this file except in compliance with the License.
|
||
|
* You may obtain a copy of the License at
|
||
|
*
|
||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
*
|
||
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
* See the License for the specific language governing permissions and
|
||
|
* limitations under the License.
|
||
|
*/
|
||
|
|
||
|
package com.android.dialer.app.voicemail;
|
||
|
|
||
|
import android.content.Context;
|
||
|
import android.graphics.drawable.Drawable;
|
||
|
import android.net.Uri;
|
||
|
import android.os.Handler;
|
||
|
import android.support.annotation.VisibleForTesting;
|
||
|
import android.support.design.widget.Snackbar;
|
||
|
import android.util.AttributeSet;
|
||
|
import android.view.LayoutInflater;
|
||
|
import android.view.View;
|
||
|
import android.widget.ImageButton;
|
||
|
import android.widget.LinearLayout;
|
||
|
import android.widget.SeekBar;
|
||
|
import android.widget.SeekBar.OnSeekBarChangeListener;
|
||
|
import android.widget.TextView;
|
||
|
import com.android.dialer.app.R;
|
||
|
import com.android.dialer.app.calllog.CallLogAsyncTaskUtil;
|
||
|
import com.android.dialer.app.calllog.CallLogListItemViewHolder;
|
||
|
import com.android.dialer.logging.DialerImpression;
|
||
|
import com.android.dialer.logging.Logger;
|
||
|
import java.util.Objects;
|
||
|
import java.util.concurrent.ScheduledExecutorService;
|
||
|
import java.util.concurrent.ScheduledFuture;
|
||
|
import java.util.concurrent.TimeUnit;
|
||
|
import javax.annotation.concurrent.GuardedBy;
|
||
|
import javax.annotation.concurrent.NotThreadSafe;
|
||
|
import javax.annotation.concurrent.ThreadSafe;
|
||
|
|
||
|
/**
|
||
|
* Displays and plays a single voicemail. See {@link VoicemailPlaybackPresenter} for details on the
|
||
|
* voicemail playback implementation.
|
||
|
*
|
||
|
* <p>This class is not thread-safe, it is thread-confined. All calls to all public methods on this
|
||
|
* class are expected to come from the main ui thread.
|
||
|
*/
|
||
|
@NotThreadSafe
|
||
|
public class VoicemailPlaybackLayout extends LinearLayout
|
||
|
implements VoicemailPlaybackPresenter.PlaybackView,
|
||
|
CallLogAsyncTaskUtil.CallLogAsyncTaskListener {
|
||
|
|
||
|
private static final String TAG = VoicemailPlaybackLayout.class.getSimpleName();
|
||
|
private static final int VOICEMAIL_DELETE_DELAY_MS = 3000;
|
||
|
|
||
|
private Context context;
|
||
|
private CallLogListItemViewHolder viewHolder;
|
||
|
private VoicemailPlaybackPresenter presenter;
|
||
|
/** Click listener to toggle speakerphone. */
|
||
|
private final View.OnClickListener speakerphoneListener =
|
||
|
new View.OnClickListener() {
|
||
|
@Override
|
||
|
public void onClick(View v) {
|
||
|
if (presenter != null) {
|
||
|
presenter.toggleSpeakerphone();
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
private Uri voicemailUri;
|
||
|
private final View.OnClickListener deleteButtonListener =
|
||
|
new View.OnClickListener() {
|
||
|
@Override
|
||
|
public void onClick(View view) {
|
||
|
Logger.get(context).logImpression(DialerImpression.Type.VOICEMAIL_DELETE_ENTRY);
|
||
|
if (presenter == null) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// When the undo button is pressed, the viewHolder we have is no longer valid because when
|
||
|
// we hide the view it is binded to something else, and the layout is not updated for
|
||
|
// hidden items. copy the adapter position so we can update the view upon undo.
|
||
|
// TODO(twyen): refactor this so the view holder will always be valid.
|
||
|
final int adapterPosition = viewHolder.getAdapterPosition();
|
||
|
|
||
|
presenter.pausePlayback();
|
||
|
presenter.onVoicemailDeleted(viewHolder);
|
||
|
|
||
|
final Uri deleteUri = voicemailUri;
|
||
|
final Runnable deleteCallback =
|
||
|
new Runnable() {
|
||
|
@Override
|
||
|
public void run() {
|
||
|
if (Objects.equals(deleteUri, voicemailUri)) {
|
||
|
CallLogAsyncTaskUtil.deleteVoicemail(
|
||
|
context, deleteUri, VoicemailPlaybackLayout.this);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
final Handler handler = new Handler();
|
||
|
// Add a little buffer time in case the user clicked "undo" at the end of the delay
|
||
|
// window.
|
||
|
handler.postDelayed(deleteCallback, VOICEMAIL_DELETE_DELAY_MS + 50);
|
||
|
|
||
|
Snackbar.make(
|
||
|
VoicemailPlaybackLayout.this,
|
||
|
R.string.snackbar_voicemail_deleted,
|
||
|
Snackbar.LENGTH_LONG)
|
||
|
.setDuration(VOICEMAIL_DELETE_DELAY_MS)
|
||
|
.setAction(
|
||
|
R.string.snackbar_undo,
|
||
|
new View.OnClickListener() {
|
||
|
@Override
|
||
|
public void onClick(View view) {
|
||
|
presenter.onVoicemailDeleteUndo(adapterPosition);
|
||
|
handler.removeCallbacks(deleteCallback);
|
||
|
}
|
||
|
})
|
||
|
.setActionTextColor(
|
||
|
context.getResources().getColor(R.color.dialer_snackbar_action_text_color))
|
||
|
.show();
|
||
|
}
|
||
|
};
|
||
|
private boolean isPlaying = false;
|
||
|
/** Click listener to play or pause voicemail playback. */
|
||
|
private final View.OnClickListener startStopButtonListener =
|
||
|
new View.OnClickListener() {
|
||
|
@Override
|
||
|
public void onClick(View view) {
|
||
|
if (presenter == null) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (isPlaying) {
|
||
|
presenter.pausePlayback();
|
||
|
} else {
|
||
|
Logger.get(context)
|
||
|
.logImpression(DialerImpression.Type.VOICEMAIL_PLAY_AUDIO_AFTER_EXPANDING_ENTRY);
|
||
|
presenter.resumePlayback();
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
private SeekBar playbackSeek;
|
||
|
private ImageButton startStopButton;
|
||
|
private ImageButton playbackSpeakerphone;
|
||
|
private ImageButton deleteButton;
|
||
|
private TextView stateText;
|
||
|
private TextView positionText;
|
||
|
private TextView totalDurationText;
|
||
|
/** Handle state changes when the user manipulates the seek bar. */
|
||
|
private final OnSeekBarChangeListener seekBarChangeListener =
|
||
|
new OnSeekBarChangeListener() {
|
||
|
@Override
|
||
|
public void onStartTrackingTouch(SeekBar seekBar) {
|
||
|
if (presenter != null) {
|
||
|
presenter.pausePlaybackForSeeking();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onStopTrackingTouch(SeekBar seekBar) {
|
||
|
if (presenter != null) {
|
||
|
presenter.resumePlaybackAfterSeeking(seekBar.getProgress());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||
|
setClipPosition(progress, seekBar.getMax());
|
||
|
// Update the seek position if user manually changed it. This makes sure position gets
|
||
|
// updated when user use volume button to seek playback in talkback mode.
|
||
|
if (fromUser) {
|
||
|
presenter.seek(progress);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
private PositionUpdater positionUpdater;
|
||
|
private Drawable voicemailSeekHandleEnabled;
|
||
|
private Drawable voicemailSeekHandleDisabled;
|
||
|
|
||
|
public VoicemailPlaybackLayout(Context context) {
|
||
|
this(context, null);
|
||
|
}
|
||
|
|
||
|
public VoicemailPlaybackLayout(Context context, AttributeSet attrs) {
|
||
|
super(context, attrs);
|
||
|
this.context = context;
|
||
|
LayoutInflater inflater =
|
||
|
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||
|
inflater.inflate(R.layout.voicemail_playback_layout, this);
|
||
|
}
|
||
|
|
||
|
public void setViewHolder(CallLogListItemViewHolder mViewHolder) {
|
||
|
this.viewHolder = mViewHolder;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri) {
|
||
|
this.presenter = presenter;
|
||
|
this.voicemailUri = voicemailUri;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected void onFinishInflate() {
|
||
|
super.onFinishInflate();
|
||
|
|
||
|
playbackSeek = (SeekBar) findViewById(R.id.playback_seek);
|
||
|
startStopButton = (ImageButton) findViewById(R.id.playback_start_stop);
|
||
|
playbackSpeakerphone = (ImageButton) findViewById(R.id.playback_speakerphone);
|
||
|
deleteButton = (ImageButton) findViewById(R.id.delete_voicemail);
|
||
|
|
||
|
stateText = (TextView) findViewById(R.id.playback_state_text);
|
||
|
stateText.setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
|
||
|
positionText = (TextView) findViewById(R.id.playback_position_text);
|
||
|
totalDurationText = (TextView) findViewById(R.id.total_duration_text);
|
||
|
|
||
|
playbackSeek.setOnSeekBarChangeListener(seekBarChangeListener);
|
||
|
startStopButton.setOnClickListener(startStopButtonListener);
|
||
|
playbackSpeakerphone.setOnClickListener(speakerphoneListener);
|
||
|
deleteButton.setOnClickListener(deleteButtonListener);
|
||
|
|
||
|
positionText.setText(formatAsMinutesAndSeconds(0));
|
||
|
totalDurationText.setText(formatAsMinutesAndSeconds(0));
|
||
|
|
||
|
voicemailSeekHandleEnabled =
|
||
|
getResources().getDrawable(R.drawable.ic_voicemail_seek_handle, context.getTheme());
|
||
|
voicemailSeekHandleDisabled =
|
||
|
getResources()
|
||
|
.getDrawable(R.drawable.old_ic_voicemail_seek_handle_disabled, context.getTheme());
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onPlaybackStarted(int duration, ScheduledExecutorService executorService) {
|
||
|
isPlaying = true;
|
||
|
|
||
|
startStopButton.setImageResource(R.drawable.ic_pause);
|
||
|
|
||
|
if (positionUpdater != null) {
|
||
|
positionUpdater.stopUpdating();
|
||
|
positionUpdater = null;
|
||
|
}
|
||
|
positionUpdater = new PositionUpdater(duration, executorService);
|
||
|
positionUpdater.startUpdating();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onPlaybackStopped() {
|
||
|
isPlaying = false;
|
||
|
|
||
|
startStopButton.setImageResource(R.drawable.ic_play_arrow);
|
||
|
|
||
|
if (positionUpdater != null) {
|
||
|
positionUpdater.stopUpdating();
|
||
|
positionUpdater = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onPlaybackError() {
|
||
|
if (positionUpdater != null) {
|
||
|
positionUpdater.stopUpdating();
|
||
|
}
|
||
|
|
||
|
disableUiElements();
|
||
|
stateText.setText(getString(R.string.voicemail_playback_error));
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onSpeakerphoneOn(boolean on) {
|
||
|
if (on) {
|
||
|
playbackSpeakerphone.setImageResource(R.drawable.quantum_ic_volume_up_vd_theme_24);
|
||
|
// Speaker is now on, tapping button will turn it off.
|
||
|
playbackSpeakerphone.setContentDescription(context.getString(R.string.voicemail_speaker_off));
|
||
|
} else {
|
||
|
playbackSpeakerphone.setImageResource(R.drawable.quantum_ic_volume_down_white_24);
|
||
|
// Speaker is now off, tapping button will turn it on.
|
||
|
playbackSpeakerphone.setContentDescription(context.getString(R.string.voicemail_speaker_on));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void setClipPosition(int positionMs, int durationMs) {
|
||
|
int seekBarPositionMs = Math.max(0, positionMs);
|
||
|
int seekBarMax = Math.max(seekBarPositionMs, durationMs);
|
||
|
if (playbackSeek.getMax() != seekBarMax) {
|
||
|
playbackSeek.setMax(seekBarMax);
|
||
|
}
|
||
|
|
||
|
playbackSeek.setProgress(seekBarPositionMs);
|
||
|
|
||
|
positionText.setText(formatAsMinutesAndSeconds(seekBarPositionMs));
|
||
|
totalDurationText.setText(formatAsMinutesAndSeconds(durationMs));
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void setSuccess() {
|
||
|
stateText.setText(null);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void setIsFetchingContent() {
|
||
|
disableUiElements();
|
||
|
stateText.setText(getString(R.string.voicemail_fetching_content));
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void setFetchContentTimeout() {
|
||
|
startStopButton.setEnabled(true);
|
||
|
stateText.setText(getString(R.string.voicemail_fetching_timout));
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getDesiredClipPosition() {
|
||
|
return playbackSeek.getProgress();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void disableUiElements() {
|
||
|
startStopButton.setEnabled(false);
|
||
|
resetSeekBar();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void enableUiElements() {
|
||
|
deleteButton.setEnabled(true);
|
||
|
startStopButton.setEnabled(true);
|
||
|
playbackSeek.setEnabled(true);
|
||
|
playbackSeek.setThumb(voicemailSeekHandleEnabled);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void resetSeekBar() {
|
||
|
playbackSeek.setProgress(0);
|
||
|
playbackSeek.setEnabled(false);
|
||
|
playbackSeek.setThumb(voicemailSeekHandleDisabled);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onDeleteVoicemail() {
|
||
|
presenter.onVoicemailDeletedInDatabase();
|
||
|
}
|
||
|
|
||
|
private String getString(int resId) {
|
||
|
return context.getString(resId);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Formats a number of milliseconds as something that looks like {@code 00:05}.
|
||
|
*
|
||
|
* <p>We always use four digits, two for minutes two for seconds. In the very unlikely event that
|
||
|
* the voicemail duration exceeds 99 minutes, the display is capped at 99 minutes.
|
||
|
*/
|
||
|
private String formatAsMinutesAndSeconds(int millis) {
|
||
|
int seconds = millis / 1000;
|
||
|
int minutes = seconds / 60;
|
||
|
seconds -= minutes * 60;
|
||
|
if (minutes > 99) {
|
||
|
minutes = 99;
|
||
|
}
|
||
|
return String.format("%02d:%02d", minutes, seconds);
|
||
|
}
|
||
|
|
||
|
@VisibleForTesting
|
||
|
public String getStateText() {
|
||
|
return stateText.getText().toString();
|
||
|
}
|
||
|
|
||
|
/** Controls the animation of the playback slider. */
|
||
|
@ThreadSafe
|
||
|
private final class PositionUpdater implements Runnable {
|
||
|
|
||
|
/** Update rate for the slider, 30fps. */
|
||
|
private static final int SLIDER_UPDATE_PERIOD_MILLIS = 1000 / 30;
|
||
|
|
||
|
private final ScheduledExecutorService executorService;
|
||
|
private final Object lock = new Object();
|
||
|
private int durationMs;
|
||
|
|
||
|
@GuardedBy("lock")
|
||
|
private ScheduledFuture<?> scheduledFuture;
|
||
|
|
||
|
private Runnable updateClipPositionRunnable =
|
||
|
new Runnable() {
|
||
|
@Override
|
||
|
public void run() {
|
||
|
int currentPositionMs = 0;
|
||
|
synchronized (lock) {
|
||
|
if (scheduledFuture == null || presenter == null) {
|
||
|
// This task has been canceled. Just stop now.
|
||
|
return;
|
||
|
}
|
||
|
currentPositionMs = presenter.getMediaPlayerPosition();
|
||
|
}
|
||
|
setClipPosition(currentPositionMs, durationMs);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
public PositionUpdater(int durationMs, ScheduledExecutorService executorService) {
|
||
|
this.durationMs = durationMs;
|
||
|
this.executorService = executorService;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void run() {
|
||
|
post(updateClipPositionRunnable);
|
||
|
}
|
||
|
|
||
|
public void startUpdating() {
|
||
|
synchronized (lock) {
|
||
|
cancelPendingRunnables();
|
||
|
scheduledFuture =
|
||
|
executorService.scheduleAtFixedRate(
|
||
|
this, 0, SLIDER_UPDATE_PERIOD_MILLIS, TimeUnit.MILLISECONDS);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public void stopUpdating() {
|
||
|
synchronized (lock) {
|
||
|
cancelPendingRunnables();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@GuardedBy("lock")
|
||
|
private void cancelPendingRunnables() {
|
||
|
if (scheduledFuture != null) {
|
||
|
scheduledFuture.cancel(true);
|
||
|
scheduledFuture = null;
|
||
|
}
|
||
|
removeCallbacks(updateClipPositionRunnable);
|
||
|
}
|
||
|
}
|
||
|
}
|