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