Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2015 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 package com.android.messaging.ui;
     17 
     18 import android.content.Context;
     19 import android.content.res.TypedArray;
     20 import android.graphics.Canvas;
     21 import android.graphics.Path;
     22 import android.graphics.RectF;
     23 import android.media.AudioManager;
     24 import android.media.MediaPlayer;
     25 import android.media.MediaPlayer.OnCompletionListener;
     26 import android.media.MediaPlayer.OnErrorListener;
     27 import android.media.MediaPlayer.OnPreparedListener;
     28 import android.net.Uri;
     29 import android.os.SystemClock;
     30 import android.text.TextUtils;
     31 import android.util.AttributeSet;
     32 import android.view.LayoutInflater;
     33 import android.view.View;
     34 import android.widget.ImageView;
     35 import android.widget.LinearLayout;
     36 
     37 import com.android.messaging.Factory;
     38 import com.android.messaging.R;
     39 import com.android.messaging.datamodel.data.MessagePartData;
     40 import com.android.messaging.ui.mediapicker.PausableChronometer;
     41 import com.android.messaging.util.Assert;
     42 import com.android.messaging.util.ContentType;
     43 import com.android.messaging.util.LogUtil;
     44 import com.android.messaging.util.MediaUtil;
     45 import com.android.messaging.util.UiUtils;
     46 
     47 /**
     48  * A reusable widget that hosts an audio player for audio attachment playback. This widget is used
     49  * by both the media picker and the conversation message view to show audio attachments.
     50  */
     51 public class AudioAttachmentView extends LinearLayout {
     52     /** The normal layout mode where we have the play button, timer and progress bar */
     53     private static final int LAYOUT_MODE_NORMAL = 0;
     54 
     55     /** The compact layout mode with only the play button and the timer beneath it. Suitable
     56      *  for displaying in limited space such as multi-attachment layout */
     57     private static final int LAYOUT_MODE_COMPACT = 1;
     58 
     59     /** The sub-compact layout mode with only the play button. */
     60     private static final int LAYOUT_MODE_SUB_COMPACT = 2;
     61 
     62     private static final int PLAY_BUTTON = 0;
     63     private static final int PAUSE_BUTTON = 1;
     64 
     65     private AudioAttachmentPlayPauseButton mPlayPauseButton;
     66     private PausableChronometer mChronometer;
     67     private AudioPlaybackProgressBar mProgressBar;
     68     private MediaPlayer mMediaPlayer;
     69 
     70     private Uri mDataSourceUri;
     71 
     72     // The corner radius for drawing rounded corners. The default value is zero (no rounded corners)
     73     private final int mCornerRadius;
     74     private final Path mRoundedCornerClipPath;
     75     private int mClipPathWidth;
     76     private int mClipPathHeight;
     77 
     78     private boolean mUseIncomingStyle;
     79     private int mThemeColor;
     80 
     81     private boolean mStartPlayAfterPrepare;
     82     // should the MediaPlayer be prepared lazily when the user chooses to play the audio (as
     83     // opposed to preparing it early, on bind)
     84     private boolean mPrepareOnPlayback;
     85     private boolean mPrepared;
     86     private boolean mPlaybackFinished; // Was the audio played all the way to the end
     87     private final int mMode;
     88 
     89     public AudioAttachmentView(final Context context, final AttributeSet attrs) {
     90         super(context, attrs);
     91         final TypedArray typedAttributes =
     92                 context.obtainStyledAttributes(attrs, R.styleable.AudioAttachmentView);
     93         mMode = typedAttributes.getInt(R.styleable.AudioAttachmentView_layoutMode,
     94                 LAYOUT_MODE_NORMAL);
     95         final LayoutInflater inflater = LayoutInflater.from(getContext());
     96         inflater.inflate(R.layout.audio_attachment_view, this, true);
     97         typedAttributes.recycle();
     98 
     99         setWillNotDraw(mMode != LAYOUT_MODE_SUB_COMPACT);
    100         mRoundedCornerClipPath = new Path();
    101         mCornerRadius = context.getResources().getDimensionPixelSize(
    102                 R.dimen.conversation_list_image_preview_corner_radius);
    103         setContentDescription(context.getString(R.string.audio_attachment_content_description));
    104     }
    105 
    106     @Override
    107     protected void onFinishInflate() {
    108         super.onFinishInflate();
    109 
    110         mPlayPauseButton = (AudioAttachmentPlayPauseButton) findViewById(R.id.play_pause_button);
    111         mChronometer = (PausableChronometer) findViewById(R.id.timer);
    112         mProgressBar = (AudioPlaybackProgressBar) findViewById(R.id.progress);
    113         mPlayPauseButton.setOnClickListener(new OnClickListener() {
    114             @Override
    115             public void onClick(final View v) {
    116                 // Has the MediaPlayer already been prepared?
    117                 if (mMediaPlayer != null && mPrepared) {
    118                     if (mMediaPlayer.isPlaying()) {
    119                         mMediaPlayer.pause();
    120                         mChronometer.pause();
    121                         mProgressBar.pause();
    122                     } else {
    123                         playAudio();
    124                     }
    125                 } else {
    126                     // Either eager preparation is still going on (the user must have clicked
    127                     // the Play button immediately after the view is bound) or this is lazy
    128                     // preparation.
    129                     if (mStartPlayAfterPrepare) {
    130                         // The user is (starting and) pausing before the MediaPlayer is prepared
    131                         mStartPlayAfterPrepare = false;
    132                     } else {
    133                         mStartPlayAfterPrepare = true;
    134                         setupMediaPlayer();
    135                     }
    136                 }
    137                 updatePlayPauseButtonState();
    138             }
    139         });
    140         updatePlayPauseButtonState();
    141         initializeViewsForMode();
    142     }
    143 
    144     private void updateChronometerVisibility(final boolean playing) {
    145         if (mChronometer.getVisibility() == View.GONE) {
    146             // The chronometer is always GONE for LAYOUT_MODE_SUB_COMPACT
    147             Assert.equals(LAYOUT_MODE_SUB_COMPACT, mMode);
    148             return;
    149         }
    150 
    151         if (mPrepareOnPlayback) {
    152             // For lazy preparation, the chronometer will only be shown during playback
    153             mChronometer.setVisibility(playing ? View.VISIBLE : View.INVISIBLE);
    154         } else {
    155             mChronometer.setVisibility(View.VISIBLE);
    156         }
    157     }
    158 
    159     /**
    160      * Bind the audio attachment view with a MessagePartData.
    161      * @param incoming indicates whether the attachment view is to be styled as a part of an
    162      *        incoming message.
    163      */
    164     public void bindMessagePartData(final MessagePartData messagePartData,
    165             final boolean incoming, final boolean showAsSelected) {
    166         Assert.isTrue(messagePartData == null ||
    167                 ContentType.isAudioType(messagePartData.getContentType()));
    168         final Uri contentUri = (messagePartData == null) ? null : messagePartData.getContentUri();
    169         bind(contentUri, incoming, showAsSelected);
    170     }
    171 
    172     public void bind(
    173             final Uri dataSourceUri, final boolean incoming, final boolean showAsSelected) {
    174         final String currentUriString = (mDataSourceUri == null) ? "" : mDataSourceUri.toString();
    175         final String newUriString = (dataSourceUri == null) ? "" : dataSourceUri.toString();
    176         final int themeColor = ConversationDrawables.get().getConversationThemeColor();
    177         final boolean useIncomingStyle = incoming || showAsSelected;
    178         final boolean visualStyleChanged = mThemeColor != themeColor ||
    179                 mUseIncomingStyle != useIncomingStyle;
    180 
    181         mUseIncomingStyle = useIncomingStyle;
    182         mThemeColor = themeColor;
    183         mPrepareOnPlayback = incoming && !MediaUtil.canAutoAccessIncomingMedia();
    184 
    185         if (!TextUtils.equals(currentUriString, newUriString)) {
    186             mDataSourceUri = dataSourceUri;
    187             resetToZeroState();
    188         } else if (visualStyleChanged) {
    189             updateVisualStyle();
    190         }
    191     }
    192 
    193     private void playAudio() {
    194         Assert.notNull(mMediaPlayer);
    195         if (mPlaybackFinished) {
    196             mMediaPlayer.seekTo(0);
    197             mChronometer.restart();
    198             mProgressBar.restart();
    199             mPlaybackFinished = false;
    200         } else {
    201             mChronometer.resume();
    202             mProgressBar.resume();
    203         }
    204         mMediaPlayer.start();
    205     }
    206 
    207     private void onAudioReplayError(final int what, final int extra, final Exception exception) {
    208         if (exception == null) {
    209             LogUtil.e(LogUtil.BUGLE_TAG, "audio replay failed, what=" + what +
    210                     ", extra=" + extra);
    211         } else {
    212             LogUtil.e(LogUtil.BUGLE_TAG, "audio replay failed, exception=" + exception);
    213         }
    214         UiUtils.showToastAtBottom(R.string.audio_recording_replay_failed);
    215         releaseMediaPlayer();
    216     }
    217 
    218     /**
    219      * Prepare the MediaPlayer, and if mPrepareOnPlayback, start playing the audio
    220      */
    221     private void setupMediaPlayer() {
    222         Assert.notNull(mDataSourceUri);
    223         if (mMediaPlayer == null) {
    224             Assert.isTrue(!mPrepared);
    225             mMediaPlayer = new MediaPlayer();
    226 
    227             try {
    228                 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
    229                 mMediaPlayer.setDataSource(Factory.get().getApplicationContext(), mDataSourceUri);
    230                 mMediaPlayer.setOnCompletionListener(new OnCompletionListener() {
    231                     @Override
    232                     public void onCompletion(final MediaPlayer mp) {
    233                         updatePlayPauseButtonState();
    234                         mChronometer.reset();
    235                         mChronometer.setBase(SystemClock.elapsedRealtime() -
    236                                 mMediaPlayer.getDuration());
    237                         updateChronometerVisibility(false /* playing */);
    238                         mProgressBar.reset();
    239 
    240                         mPlaybackFinished = true;
    241                     }
    242                 });
    243 
    244                 mMediaPlayer.setOnPreparedListener(new OnPreparedListener() {
    245                     @Override
    246                     public void onPrepared(final MediaPlayer mp) {
    247                         // Set base on the chronometer so we can show the full length of the audio.
    248                         mChronometer.setBase(SystemClock.elapsedRealtime() -
    249                                 mMediaPlayer.getDuration());
    250                         mProgressBar.setDuration(mMediaPlayer.getDuration());
    251                         mMediaPlayer.seekTo(0);
    252                         mPrepared = true;
    253 
    254                         if (mStartPlayAfterPrepare) {
    255                             mStartPlayAfterPrepare = false;
    256                             playAudio();
    257                             updatePlayPauseButtonState();
    258                         }
    259                     }
    260                 });
    261 
    262                 mMediaPlayer.setOnErrorListener(new OnErrorListener() {
    263                     @Override
    264                     public boolean onError(final MediaPlayer mp, final int what, final int extra) {
    265                         mStartPlayAfterPrepare = false;
    266                         onAudioReplayError(what, extra, null);
    267                         return true;
    268                     }
    269                 });
    270 
    271                 mMediaPlayer.prepareAsync();
    272             } catch (final Exception exception) {
    273                 onAudioReplayError(0, 0, exception);
    274                 releaseMediaPlayer();
    275             }
    276         }
    277     }
    278 
    279     private void releaseMediaPlayer() {
    280         if (mMediaPlayer != null) {
    281             mMediaPlayer.release();
    282             mMediaPlayer = null;
    283             mPrepared = false;
    284             mStartPlayAfterPrepare = false;
    285             mPlaybackFinished = false;
    286             mChronometer.reset();
    287             mProgressBar.reset();
    288         }
    289     }
    290 
    291     @Override
    292     protected void onDetachedFromWindow() {
    293         super.onDetachedFromWindow();
    294         // The view must have scrolled off. Stop playback.
    295         releaseMediaPlayer();
    296     }
    297 
    298     @Override
    299     protected void onDraw(final Canvas canvas) {
    300         if (mMode != LAYOUT_MODE_SUB_COMPACT) {
    301             return;
    302         }
    303 
    304         final int currentWidth = this.getWidth();
    305         final int currentHeight = this.getHeight();
    306         if (mClipPathWidth != currentWidth || mClipPathHeight != currentHeight) {
    307             final RectF rect = new RectF(0, 0, currentWidth, currentHeight);
    308             mRoundedCornerClipPath.reset();
    309             mRoundedCornerClipPath.addRoundRect(rect, mCornerRadius, mCornerRadius,
    310                     Path.Direction.CW);
    311             mClipPathWidth = currentWidth;
    312             mClipPathHeight = currentHeight;
    313         }
    314 
    315         canvas.clipPath(mRoundedCornerClipPath);
    316         super.onDraw(canvas);
    317     }
    318 
    319     private void updatePlayPauseButtonState() {
    320         final boolean playing = mMediaPlayer != null && mMediaPlayer.isPlaying();
    321         updateChronometerVisibility(playing);
    322         if (mStartPlayAfterPrepare || playing) {
    323             mPlayPauseButton.setDisplayedChild(PAUSE_BUTTON);
    324         } else {
    325             mPlayPauseButton.setDisplayedChild(PLAY_BUTTON);
    326         }
    327     }
    328 
    329     private void resetToZeroState() {
    330         // Release the media player so it may be set up with the new audio source.
    331         releaseMediaPlayer();
    332         updateVisualStyle();
    333         updateChronometerVisibility(false /* playing */);
    334 
    335         if (mDataSourceUri != null && !mPrepareOnPlayback) {
    336             // Prepare the media player, so we can read the duration of the audio.
    337             setupMediaPlayer();
    338         }
    339     }
    340 
    341     private void updateVisualStyle() {
    342         if (mMode == LAYOUT_MODE_SUB_COMPACT) {
    343             // Sub-compact mode has static visual appearance already set up during initialization.
    344             return;
    345         }
    346 
    347         if (mUseIncomingStyle) {
    348             mChronometer.setTextColor(getResources().getColor(R.color.message_text_color_incoming));
    349         } else {
    350             mChronometer.setTextColor(getResources().getColor(R.color.message_text_color_outgoing));
    351         }
    352         mProgressBar.setVisualStyle(mUseIncomingStyle);
    353         mPlayPauseButton.setVisualStyle(mUseIncomingStyle);
    354         updatePlayPauseButtonState();
    355     }
    356 
    357     private void initializeViewsForMode() {
    358         switch (mMode) {
    359             case LAYOUT_MODE_NORMAL:
    360                 setOrientation(HORIZONTAL);
    361                 mProgressBar.setVisibility(VISIBLE);
    362                 break;
    363 
    364             case LAYOUT_MODE_COMPACT:
    365                 setOrientation(VERTICAL);
    366                 mProgressBar.setVisibility(GONE);
    367                 ((MarginLayoutParams) mPlayPauseButton.getLayoutParams()).setMargins(0, 0, 0, 0);
    368                 ((MarginLayoutParams) mChronometer.getLayoutParams()).setMargins(0, 0, 0, 0);
    369                 break;
    370 
    371             case LAYOUT_MODE_SUB_COMPACT:
    372                 setOrientation(VERTICAL);
    373                 mProgressBar.setVisibility(GONE);
    374                 mChronometer.setVisibility(GONE);
    375                 ((MarginLayoutParams) mPlayPauseButton.getLayoutParams()).setMargins(0, 0, 0, 0);
    376                 final ImageView playButton = (ImageView) findViewById(R.id.play_button);
    377                 playButton.setImageDrawable(
    378                         getResources().getDrawable(R.drawable.ic_preview_play));
    379                 final ImageView pauseButton = (ImageView) findViewById(R.id.pause_button);
    380                 pauseButton.setImageDrawable(
    381                         getResources().getDrawable(R.drawable.ic_preview_pause));
    382                 break;
    383 
    384             default:
    385                 Assert.fail("Unsupported mode for AudioAttachmentView!");
    386                 break;
    387         }
    388     }
    389 }
    390