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 */
16package com.android.messaging.ui;
17
18import android.content.Context;
19import android.content.res.TypedArray;
20import android.graphics.Canvas;
21import android.graphics.Path;
22import android.graphics.RectF;
23import android.media.AudioManager;
24import android.media.MediaPlayer;
25import android.media.MediaPlayer.OnCompletionListener;
26import android.media.MediaPlayer.OnErrorListener;
27import android.media.MediaPlayer.OnPreparedListener;
28import android.net.Uri;
29import android.os.SystemClock;
30import android.text.TextUtils;
31import android.util.AttributeSet;
32import android.view.LayoutInflater;
33import android.view.View;
34import android.widget.ImageView;
35import android.widget.LinearLayout;
36
37import com.android.messaging.Factory;
38import com.android.messaging.R;
39import com.android.messaging.datamodel.data.MessagePartData;
40import com.android.messaging.ui.mediapicker.PausableChronometer;
41import com.android.messaging.util.Assert;
42import com.android.messaging.util.ContentType;
43import com.android.messaging.util.LogUtil;
44import com.android.messaging.util.MediaUtil;
45import 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 */
51public 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