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.mediapicker; 17 18 import android.content.Context; 19 import android.graphics.Color; 20 import android.graphics.PorterDuff; 21 import android.graphics.Rect; 22 import android.graphics.Typeface; 23 import android.graphics.drawable.Drawable; 24 import android.graphics.drawable.GradientDrawable; 25 import android.media.MediaRecorder; 26 import android.net.Uri; 27 import android.util.AttributeSet; 28 import android.view.MotionEvent; 29 import android.view.View; 30 import android.widget.FrameLayout; 31 import android.widget.ImageView; 32 import android.widget.TextView; 33 34 import com.android.messaging.Factory; 35 import com.android.messaging.R; 36 import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageSubscriptionDataProvider; 37 import com.android.messaging.datamodel.data.MediaPickerMessagePartData; 38 import com.android.messaging.datamodel.data.MessagePartData; 39 import com.android.messaging.sms.MmsConfig; 40 import com.android.messaging.util.Assert; 41 import com.android.messaging.util.ContentType; 42 import com.android.messaging.util.LogUtil; 43 import com.android.messaging.util.MediaUtil; 44 import com.android.messaging.util.MediaUtil.OnCompletionListener; 45 import com.android.messaging.util.SafeAsyncTask; 46 import com.android.messaging.util.ThreadUtil; 47 import com.android.messaging.util.UiUtils; 48 import com.google.common.annotations.VisibleForTesting; 49 50 /** 51 * Hosts an audio recorder with tap and hold to record functionality. 52 */ 53 public class AudioRecordView extends FrameLayout implements 54 MediaRecorder.OnErrorListener, 55 MediaRecorder.OnInfoListener { 56 /** 57 * An interface that communicates with the hosted AudioRecordView. 58 */ 59 public interface HostInterface extends DraftMessageSubscriptionDataProvider { onAudioRecorded(final MessagePartData item)60 void onAudioRecorded(final MessagePartData item); 61 } 62 63 /** The initial state, the user may press and hold to start recording */ 64 private static final int MODE_IDLE = 1; 65 66 /** The user has pressed the record button and we are playing the sound indicating the 67 * start of recording session. Don't record yet since we don't want the beeping sound 68 * to get into the recording. */ 69 private static final int MODE_STARTING = 2; 70 71 /** When the user is actively recording */ 72 private static final int MODE_RECORDING = 3; 73 74 /** When the user has finished recording, we need to record for some additional time. */ 75 private static final int MODE_STOPPING = 4; 76 77 // Bug: 16020175: The framework's MediaRecorder would cut off the ending portion of the 78 // recorded audio by about half a second. To mitigate this issue, we continue the recording 79 // for some extra time before stopping it. 80 private static final int AUDIO_RECORD_ENDING_BUFFER_MILLIS = 500; 81 82 /** 83 * The minimum duration of any recording. Below this threshold, it will be treated as if the 84 * user clicked the record button and inform the user to tap and hold to record. 85 */ 86 private static final int AUDIO_RECORD_MINIMUM_DURATION_MILLIS = 300; 87 88 // For accessibility, the touchable record button is bigger than the record button visual. 89 private ImageView mRecordButtonVisual; 90 private View mRecordButton; 91 private SoundLevels mSoundLevels; 92 private TextView mHintTextView; 93 private PausableChronometer mTimerTextView; 94 private LevelTrackingMediaRecorder mMediaRecorder; 95 private long mAudioRecordStartTimeMillis; 96 97 private int mCurrentMode = MODE_IDLE; 98 private HostInterface mHostInterface; 99 private int mThemeColor; 100 AudioRecordView(final Context context, final AttributeSet attrs)101 public AudioRecordView(final Context context, final AttributeSet attrs) { 102 super(context, attrs); 103 mMediaRecorder = new LevelTrackingMediaRecorder(); 104 } 105 setHostInterface(final HostInterface hostInterface)106 public void setHostInterface(final HostInterface hostInterface) { 107 mHostInterface = hostInterface; 108 } 109 110 @VisibleForTesting testSetMediaRecorder(final LevelTrackingMediaRecorder recorder)111 public void testSetMediaRecorder(final LevelTrackingMediaRecorder recorder) { 112 mMediaRecorder = recorder; 113 } 114 115 @Override onFinishInflate()116 protected void onFinishInflate() { 117 super.onFinishInflate(); 118 mSoundLevels = (SoundLevels) findViewById(R.id.sound_levels); 119 mRecordButtonVisual = (ImageView) findViewById(R.id.record_button_visual); 120 mRecordButton = findViewById(R.id.record_button); 121 mHintTextView = (TextView) findViewById(R.id.hint_text); 122 mTimerTextView = (PausableChronometer) findViewById(R.id.timer_text); 123 mSoundLevels.setLevelSource(mMediaRecorder.getLevelSource()); 124 mRecordButton.setOnTouchListener(new OnTouchListener() { 125 @Override 126 public boolean onTouch(final View v, final MotionEvent event) { 127 final int action = event.getActionMasked(); 128 switch (action) { 129 case MotionEvent.ACTION_DOWN: 130 onRecordButtonTouchDown(); 131 132 // Don't let the record button handle the down event to let it fall through 133 // so that we can handle it for the entire panel in onTouchEvent(). This is 134 // done so that: 1) the user taps on the record button to start recording 135 // 2) the entire panel owns the touch event so we'd keep recording even 136 // if the user moves outside the button region. 137 return false; 138 } 139 return false; 140 } 141 }); 142 } 143 144 @Override onTouchEvent(final MotionEvent event)145 public boolean onTouchEvent(final MotionEvent event) { 146 final int action = event.getActionMasked(); 147 switch (action) { 148 case MotionEvent.ACTION_DOWN: 149 return shouldHandleTouch(); 150 151 case MotionEvent.ACTION_MOVE: 152 return true; 153 154 case MotionEvent.ACTION_UP: 155 case MotionEvent.ACTION_CANCEL: 156 return onRecordButtonTouchUp(); 157 } 158 return super.onTouchEvent(event); 159 } 160 onPause()161 public void onPause() { 162 // The conversation draft cannot take any updates when it's paused. Therefore, forcibly 163 // stop recording on pause. 164 stopRecording(); 165 } 166 167 @Override onDetachedFromWindow()168 protected void onDetachedFromWindow() { 169 super.onDetachedFromWindow(); 170 stopRecording(); 171 } 172 isRecording()173 private boolean isRecording() { 174 return mMediaRecorder.isRecording() && mCurrentMode == MODE_RECORDING; 175 } 176 shouldHandleTouch()177 public boolean shouldHandleTouch() { 178 return mCurrentMode != MODE_IDLE; 179 } 180 stopTouchHandling()181 public void stopTouchHandling() { 182 setMode(MODE_IDLE); 183 stopRecording(); 184 } 185 setMode(final int mode)186 private void setMode(final int mode) { 187 if (mCurrentMode != mode) { 188 mCurrentMode = mode; 189 updateVisualState(); 190 } 191 } 192 updateVisualState()193 private void updateVisualState() { 194 switch (mCurrentMode) { 195 case MODE_IDLE: 196 mHintTextView.setVisibility(VISIBLE); 197 mHintTextView.setTypeface(null, Typeface.NORMAL); 198 mTimerTextView.setVisibility(GONE); 199 mSoundLevels.setEnabled(false); 200 mTimerTextView.stop(); 201 break; 202 203 case MODE_RECORDING: 204 case MODE_STOPPING: 205 mHintTextView.setVisibility(GONE); 206 mTimerTextView.setVisibility(VISIBLE); 207 mSoundLevels.setEnabled(true); 208 mTimerTextView.restart(); 209 break; 210 211 case MODE_STARTING: 212 break; // No-Op. 213 214 default: 215 Assert.fail("invalid mode for AudioRecordView!"); 216 break; 217 } 218 updateRecordButtonAppearance(); 219 } 220 setThemeColor(final int color)221 public void setThemeColor(final int color) { 222 mThemeColor = color; 223 updateRecordButtonAppearance(); 224 } 225 updateRecordButtonAppearance()226 private void updateRecordButtonAppearance() { 227 final Drawable foregroundDrawable = getResources().getDrawable(R.drawable.ic_mp_audio_mic); 228 final GradientDrawable backgroundDrawable = ((GradientDrawable) getResources() 229 .getDrawable(R.drawable.audio_record_control_button_background)); 230 if (isRecording()) { 231 foregroundDrawable.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP); 232 backgroundDrawable.setColor(mThemeColor); 233 } else { 234 foregroundDrawable.setColorFilter(mThemeColor, PorterDuff.Mode.SRC_ATOP); 235 backgroundDrawable.setColor(Color.WHITE); 236 } 237 mRecordButtonVisual.setImageDrawable(foregroundDrawable); 238 mRecordButtonVisual.setBackground(backgroundDrawable); 239 } 240 241 @VisibleForTesting onRecordButtonTouchDown()242 boolean onRecordButtonTouchDown() { 243 if (!mMediaRecorder.isRecording() && mCurrentMode == MODE_IDLE) { 244 setMode(MODE_STARTING); 245 playAudioStartSound(new OnCompletionListener() { 246 @Override 247 public void onCompletion() { 248 // Double-check the current mode before recording since the user may have 249 // lifted finger from the button before the beeping sound is played through. 250 final int maxSize = MmsConfig.get(mHostInterface.getConversationSelfSubId()) 251 .getMaxMessageSize(); 252 if (mCurrentMode == MODE_STARTING && 253 mMediaRecorder.startRecording(AudioRecordView.this, 254 AudioRecordView.this, maxSize)) { 255 setMode(MODE_RECORDING); 256 } 257 } 258 }); 259 mAudioRecordStartTimeMillis = System.currentTimeMillis(); 260 return true; 261 } 262 return false; 263 } 264 265 @VisibleForTesting onRecordButtonTouchUp()266 boolean onRecordButtonTouchUp() { 267 if (System.currentTimeMillis() - mAudioRecordStartTimeMillis < 268 AUDIO_RECORD_MINIMUM_DURATION_MILLIS) { 269 // The recording is too short, bolden the hint text to instruct the user to 270 // "tap+hold" to record audio. 271 final Uri outputUri = stopRecording(); 272 if (outputUri != null) { 273 SafeAsyncTask.executeOnThreadPool(new Runnable() { 274 @Override 275 public void run() { 276 Factory.get().getApplicationContext().getContentResolver().delete( 277 outputUri, null, null); 278 } 279 }); 280 } 281 setMode(MODE_IDLE); 282 mHintTextView.setTypeface(null, Typeface.BOLD); 283 } else if (isRecording()) { 284 // Record for some extra time to ensure the ending part is saved. 285 setMode(MODE_STOPPING); 286 ThreadUtil.getMainThreadHandler().postDelayed(new Runnable() { 287 @Override 288 public void run() { 289 onFinishedRecording(); 290 } 291 }, AUDIO_RECORD_ENDING_BUFFER_MILLIS); 292 } else { 293 setMode(MODE_IDLE); 294 } 295 return true; 296 } 297 stopRecording()298 private Uri stopRecording() { 299 if (mMediaRecorder.isRecording()) { 300 return mMediaRecorder.stopRecording(); 301 } 302 return null; 303 } 304 305 @Override // From MediaRecorder.OnInfoListener onInfo(final MediaRecorder mr, final int what, final int extra)306 public void onInfo(final MediaRecorder mr, final int what, final int extra) { 307 if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) { 308 // Max size reached. Finish recording immediately. 309 LogUtil.i(LogUtil.BUGLE_TAG, "Max size reached while recording audio"); 310 onFinishedRecording(); 311 } else { 312 // These are unknown errors. 313 onErrorWhileRecording(what, extra); 314 } 315 } 316 317 @Override // From MediaRecorder.OnErrorListener onError(final MediaRecorder mr, final int what, final int extra)318 public void onError(final MediaRecorder mr, final int what, final int extra) { 319 onErrorWhileRecording(what, extra); 320 } 321 onErrorWhileRecording(final int what, final int extra)322 private void onErrorWhileRecording(final int what, final int extra) { 323 LogUtil.e(LogUtil.BUGLE_TAG, "Error occurred during audio recording what=" + what + 324 ", extra=" + extra); 325 UiUtils.showToastAtBottom(R.string.audio_recording_error); 326 setMode(MODE_IDLE); 327 stopRecording(); 328 } 329 onFinishedRecording()330 private void onFinishedRecording() { 331 final Uri outputUri = stopRecording(); 332 if (outputUri != null) { 333 final Rect startRect = new Rect(); 334 mRecordButtonVisual.getGlobalVisibleRect(startRect); 335 final MediaPickerMessagePartData audioItem = 336 new MediaPickerMessagePartData(startRect, 337 ContentType.AUDIO_3GPP, outputUri, 0, 0); 338 mHostInterface.onAudioRecorded(audioItem); 339 } 340 playAudioEndSound(); 341 setMode(MODE_IDLE); 342 } 343 playAudioStartSound(final OnCompletionListener completionListener)344 private void playAudioStartSound(final OnCompletionListener completionListener) { 345 MediaUtil.get().playSound(getContext(), R.raw.audio_initiate, completionListener); 346 } 347 playAudioEndSound()348 private void playAudioEndSound() { 349 MediaUtil.get().playSound(getContext(), R.raw.audio_end, null); 350 } 351 } 352