/* * 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; } } }