blob: ad91ed26286ee86f90a1c3e90db95d1a212bb4a1 [file] [log] [blame]
/*
* Copyright (C) 2015 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.messaging.ui;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.RectF;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnErrorListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.net.Uri;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.datamodel.data.MessagePartData;
import com.android.messaging.ui.mediapicker.PausableChronometer;
import com.android.messaging.util.Assert;
import com.android.messaging.util.ContentType;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.MediaUtil;
import com.android.messaging.util.UiUtils;
/**
* A reusable widget that hosts an audio player for audio attachment playback. This widget is used
* by both the media picker and the conversation message view to show audio attachments.
*/
public class AudioAttachmentView extends LinearLayout {
/** The normal layout mode where we have the play button, timer and progress bar */
private static final int LAYOUT_MODE_NORMAL = 0;
/** The compact layout mode with only the play button and the timer beneath it. Suitable
* for displaying in limited space such as multi-attachment layout */
private static final int LAYOUT_MODE_COMPACT = 1;
/** The sub-compact layout mode with only the play button. */
private static final int LAYOUT_MODE_SUB_COMPACT = 2;
private static final int PLAY_BUTTON = 0;
private static final int PAUSE_BUTTON = 1;
private AudioAttachmentPlayPauseButton mPlayPauseButton;
private PausableChronometer mChronometer;
private AudioPlaybackProgressBar mProgressBar;
private MediaPlayer mMediaPlayer;
private Uri mDataSourceUri;
// The corner radius for drawing rounded corners. The default value is zero (no rounded corners)
private final int mCornerRadius;
private final Path mRoundedCornerClipPath;
private int mClipPathWidth;
private int mClipPathHeight;
private boolean mUseIncomingStyle;
private int mThemeColor;
private boolean mStartPlayAfterPrepare;
// should the MediaPlayer be prepared lazily when the user chooses to play the audio (as
// opposed to preparing it early, on bind)
private boolean mPrepareOnPlayback;
private boolean mPrepared;
private boolean mPlaybackFinished; // Was the audio played all the way to the end
private final int mMode;
public AudioAttachmentView(final Context context, final AttributeSet attrs) {
super(context, attrs);
final TypedArray typedAttributes =
context.obtainStyledAttributes(attrs, R.styleable.AudioAttachmentView);
mMode = typedAttributes.getInt(R.styleable.AudioAttachmentView_layoutMode,
LAYOUT_MODE_NORMAL);
final LayoutInflater inflater = LayoutInflater.from(getContext());
inflater.inflate(R.layout.audio_attachment_view, this, true);
typedAttributes.recycle();
setWillNotDraw(mMode != LAYOUT_MODE_SUB_COMPACT);
mRoundedCornerClipPath = new Path();
mCornerRadius = context.getResources().getDimensionPixelSize(
R.dimen.conversation_list_image_preview_corner_radius);
setContentDescription(context.getString(R.string.audio_attachment_content_description));
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mPlayPauseButton = (AudioAttachmentPlayPauseButton) findViewById(R.id.play_pause_button);
mChronometer = (PausableChronometer) findViewById(R.id.timer);
mProgressBar = (AudioPlaybackProgressBar) findViewById(R.id.progress);
mPlayPauseButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
// Has the MediaPlayer already been prepared?
if (mMediaPlayer != null && mPrepared) {
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
mChronometer.pause();
mProgressBar.pause();
} else {
playAudio();
}
} else {
// Either eager preparation is still going on (the user must have clicked
// the Play button immediately after the view is bound) or this is lazy
// preparation.
if (mStartPlayAfterPrepare) {
// The user is (starting and) pausing before the MediaPlayer is prepared
mStartPlayAfterPrepare = false;
} else {
mStartPlayAfterPrepare = true;
setupMediaPlayer();
}
}
updatePlayPauseButtonState();
}
});
updatePlayPauseButtonState();
initializeViewsForMode();
}
private void updateChronometerVisibility(final boolean playing) {
if (mChronometer.getVisibility() == View.GONE) {
// The chronometer is always GONE for LAYOUT_MODE_SUB_COMPACT
Assert.equals(LAYOUT_MODE_SUB_COMPACT, mMode);
return;
}
if (mPrepareOnPlayback) {
// For lazy preparation, the chronometer will only be shown during playback
mChronometer.setVisibility(playing ? View.VISIBLE : View.INVISIBLE);
} else {
mChronometer.setVisibility(View.VISIBLE);
}
}
/**
* Bind the audio attachment view with a MessagePartData.
* @param incoming indicates whether the attachment view is to be styled as a part of an
* incoming message.
*/
public void bindMessagePartData(final MessagePartData messagePartData,
final boolean incoming, final boolean showAsSelected) {
Assert.isTrue(messagePartData == null ||
ContentType.isAudioType(messagePartData.getContentType()));
final Uri contentUri = (messagePartData == null) ? null : messagePartData.getContentUri();
bind(contentUri, incoming, showAsSelected);
}
public void bind(
final Uri dataSourceUri, final boolean incoming, final boolean showAsSelected) {
final String currentUriString = (mDataSourceUri == null) ? "" : mDataSourceUri.toString();
final String newUriString = (dataSourceUri == null) ? "" : dataSourceUri.toString();
final int themeColor = ConversationDrawables.get().getConversationThemeColor();
final boolean useIncomingStyle = incoming || showAsSelected;
final boolean visualStyleChanged = mThemeColor != themeColor ||
mUseIncomingStyle != useIncomingStyle;
mUseIncomingStyle = useIncomingStyle;
mThemeColor = themeColor;
mPrepareOnPlayback = incoming && !MediaUtil.canAutoAccessIncomingMedia();
if (!TextUtils.equals(currentUriString, newUriString)) {
mDataSourceUri = dataSourceUri;
resetToZeroState();
} else if (visualStyleChanged) {
updateVisualStyle();
}
}
private void playAudio() {
Assert.notNull(mMediaPlayer);
if (mPlaybackFinished) {
mMediaPlayer.seekTo(0);
mChronometer.restart();
mProgressBar.restart();
mPlaybackFinished = false;
} else {
mChronometer.resume();
mProgressBar.resume();
}
mMediaPlayer.start();
}
private void onAudioReplayError(final int what, final int extra, final Exception exception) {
if (exception == null) {
LogUtil.e(LogUtil.BUGLE_TAG, "audio replay failed, what=" + what +
", extra=" + extra);
} else {
LogUtil.e(LogUtil.BUGLE_TAG, "audio replay failed, exception=" + exception);
}
UiUtils.showToastAtBottom(R.string.audio_recording_replay_failed);
releaseMediaPlayer();
}
/**
* Prepare the MediaPlayer, and if mPrepareOnPlayback, start playing the audio
*/
private void setupMediaPlayer() {
Assert.notNull(mDataSourceUri);
if (mMediaPlayer == null) {
Assert.isTrue(!mPrepared);
mMediaPlayer = new MediaPlayer();
try {
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setDataSource(Factory.get().getApplicationContext(), mDataSourceUri);
mMediaPlayer.setOnCompletionListener(new OnCompletionListener() {
@Override
public void onCompletion(final MediaPlayer mp) {
updatePlayPauseButtonState();
mChronometer.reset();
mChronometer.setBase(SystemClock.elapsedRealtime() -
mMediaPlayer.getDuration());
updateChronometerVisibility(false /* playing */);
mProgressBar.reset();
mPlaybackFinished = true;
}
});
mMediaPlayer.setOnPreparedListener(new OnPreparedListener() {
@Override
public void onPrepared(final MediaPlayer mp) {
// Set base on the chronometer so we can show the full length of the audio.
mChronometer.setBase(SystemClock.elapsedRealtime() -
mMediaPlayer.getDuration());
mProgressBar.setDuration(mMediaPlayer.getDuration());
mMediaPlayer.seekTo(0);
mPrepared = true;
if (mStartPlayAfterPrepare) {
mStartPlayAfterPrepare = false;
playAudio();
updatePlayPauseButtonState();
}
}
});
mMediaPlayer.setOnErrorListener(new OnErrorListener() {
@Override
public boolean onError(final MediaPlayer mp, final int what, final int extra) {
mStartPlayAfterPrepare = false;
onAudioReplayError(what, extra, null);
return true;
}
});
mMediaPlayer.prepareAsync();
} catch (final Exception exception) {
onAudioReplayError(0, 0, exception);
releaseMediaPlayer();
}
}
}
private void releaseMediaPlayer() {
if (mMediaPlayer != null) {
mMediaPlayer.release();
mMediaPlayer = null;
mPrepared = false;
mStartPlayAfterPrepare = false;
mPlaybackFinished = false;
mChronometer.reset();
mProgressBar.reset();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
// The view must have scrolled off. Stop playback.
releaseMediaPlayer();
}
@Override
protected void onDraw(final Canvas canvas) {
if (mMode != LAYOUT_MODE_SUB_COMPACT) {
return;
}
final int currentWidth = this.getWidth();
final int currentHeight = this.getHeight();
if (mClipPathWidth != currentWidth || mClipPathHeight != currentHeight) {
final RectF rect = new RectF(0, 0, currentWidth, currentHeight);
mRoundedCornerClipPath.reset();
mRoundedCornerClipPath.addRoundRect(rect, mCornerRadius, mCornerRadius,
Path.Direction.CW);
mClipPathWidth = currentWidth;
mClipPathHeight = currentHeight;
}
canvas.clipPath(mRoundedCornerClipPath);
super.onDraw(canvas);
}
private void updatePlayPauseButtonState() {
final boolean playing = mMediaPlayer != null && mMediaPlayer.isPlaying();
updateChronometerVisibility(playing);
if (mStartPlayAfterPrepare || playing) {
mPlayPauseButton.setDisplayedChild(PAUSE_BUTTON);
} else {
mPlayPauseButton.setDisplayedChild(PLAY_BUTTON);
}
}
private void resetToZeroState() {
// Release the media player so it may be set up with the new audio source.
releaseMediaPlayer();
updateVisualStyle();
updateChronometerVisibility(false /* playing */);
if (mDataSourceUri != null && !mPrepareOnPlayback) {
// Prepare the media player, so we can read the duration of the audio.
setupMediaPlayer();
}
}
private void updateVisualStyle() {
if (mMode == LAYOUT_MODE_SUB_COMPACT) {
// Sub-compact mode has static visual appearance already set up during initialization.
return;
}
if (mUseIncomingStyle) {
mChronometer.setTextColor(getResources().getColor(R.color.message_text_color_incoming));
} else {
mChronometer.setTextColor(getResources().getColor(R.color.message_text_color_outgoing));
}
mProgressBar.setVisualStyle(mUseIncomingStyle);
mPlayPauseButton.setVisualStyle(mUseIncomingStyle);
updatePlayPauseButtonState();
}
private void initializeViewsForMode() {
switch (mMode) {
case LAYOUT_MODE_NORMAL:
setOrientation(HORIZONTAL);
mProgressBar.setVisibility(VISIBLE);
break;
case LAYOUT_MODE_COMPACT:
setOrientation(VERTICAL);
mProgressBar.setVisibility(GONE);
((MarginLayoutParams) mPlayPauseButton.getLayoutParams()).setMargins(0, 0, 0, 0);
((MarginLayoutParams) mChronometer.getLayoutParams()).setMargins(0, 0, 0, 0);
break;
case LAYOUT_MODE_SUB_COMPACT:
setOrientation(VERTICAL);
mProgressBar.setVisibility(GONE);
mChronometer.setVisibility(GONE);
((MarginLayoutParams) mPlayPauseButton.getLayoutParams()).setMargins(0, 0, 0, 0);
final ImageView playButton = (ImageView) findViewById(R.id.play_button);
playButton.setImageDrawable(
getResources().getDrawable(R.drawable.ic_preview_play));
final ImageView pauseButton = (ImageView) findViewById(R.id.pause_button);
pauseButton.setImageDrawable(
getResources().getDrawable(R.drawable.ic_preview_pause));
break;
default:
Assert.fail("Unsupported mode for AudioAttachmentView!");
break;
}
}
}