* Remove redundant casts * getParcelable(ArrayList|Exra) without class argument is deprecated * ViewPager.setOnPageChangeListener -> addOnPageChangeListener * RecyclerView.setOnScrollListener -> addOnScrollListener * Remove unused initializations * Remove unused code Change-Id: I21beb6c90c675a4f2cfd7e3d5ebbd18b745f5911
371 lines
14 KiB
Java
371 lines
14 KiB
Java
/*
|
|
* Copyright (C) 2015 The Android Open Source Project
|
|
* Copyright (C) 2024 The LineageOS 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.Resources;
|
|
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.net.Uri;
|
|
import android.os.SystemClock;
|
|
import android.support.v7.mms.pdu.ContentType;
|
|
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 androidx.core.content.res.ResourcesCompat;
|
|
|
|
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.LogUtil;
|
|
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;
|
|
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 = findViewById(R.id.play_pause_button);
|
|
mChronometer = findViewById(R.id.timer);
|
|
mProgressBar = findViewById(R.id.progress);
|
|
mPlayPauseButton.setOnClickListener(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;
|
|
}
|
|
|
|
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;
|
|
|
|
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 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(mp -> {
|
|
updatePlayPauseButtonState();
|
|
mChronometer.reset();
|
|
mChronometer.setBase(SystemClock.elapsedRealtime() -
|
|
mMediaPlayer.getDuration());
|
|
updateChronometerVisibility(false /* playing */);
|
|
mProgressBar.reset();
|
|
|
|
mPlaybackFinished = true;
|
|
});
|
|
|
|
mMediaPlayer.setOnPreparedListener(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((mp, what, 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) {
|
|
// 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 = findViewById(R.id.play_button);
|
|
final Resources res = getResources();
|
|
final Resources.Theme theme = getContext().getTheme();
|
|
playButton.setImageDrawable(
|
|
ResourcesCompat.getDrawable(res, R.drawable.ic_preview_play, theme));
|
|
final ImageView pauseButton = findViewById(R.id.pause_button);
|
|
pauseButton.setImageDrawable(
|
|
ResourcesCompat.getDrawable(res, R.drawable.ic_preview_pause, theme));
|
|
break;
|
|
|
|
default:
|
|
Assert.fail("Unsupported mode for AudioAttachmentView!");
|
|
break;
|
|
}
|
|
}
|
|
}
|