• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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