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 
AudioAttachmentView(final Context context, final AttributeSet attrs)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
onFinishInflate()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 
updateChronometerVisibility(final boolean playing)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      */
bindMessagePartData(final MessagePartData messagePartData, final boolean incoming, final boolean showAsSelected)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 
bind( final Uri dataSourceUri, final boolean incoming, final boolean showAsSelected)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 
playAudio()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 
onAudioReplayError(final int what, final int extra, final Exception exception)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      */
setupMediaPlayer()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 
releaseMediaPlayer()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
onDetachedFromWindow()292     protected void onDetachedFromWindow() {
293         super.onDetachedFromWindow();
294         // The view must have scrolled off. Stop playback.
295         releaseMediaPlayer();
296     }
297 
298     @Override
onDraw(final Canvas canvas)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 
updatePlayPauseButtonState()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 
resetToZeroState()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 
updateVisualStyle()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 
initializeViewsForMode()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