Home | History | Annotate | Download | only in mediapicker
      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 {
     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 
    101     public AudioRecordView(final Context context, final AttributeSet attrs) {
    102         super(context, attrs);
    103         mMediaRecorder = new LevelTrackingMediaRecorder();
    104     }
    105 
    106     public void setHostInterface(final HostInterface hostInterface) {
    107         mHostInterface = hostInterface;
    108     }
    109 
    110     @VisibleForTesting
    111     public void testSetMediaRecorder(final LevelTrackingMediaRecorder recorder) {
    112         mMediaRecorder = recorder;
    113     }
    114 
    115     @Override
    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
    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 
    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
    168     protected void onDetachedFromWindow() {
    169         super.onDetachedFromWindow();
    170         stopRecording();
    171     }
    172 
    173     private boolean isRecording() {
    174         return mMediaRecorder.isRecording() && mCurrentMode == MODE_RECORDING;
    175     }
    176 
    177     public boolean shouldHandleTouch() {
    178         return mCurrentMode != MODE_IDLE;
    179     }
    180 
    181     public void stopTouchHandling() {
    182         setMode(MODE_IDLE);
    183         stopRecording();
    184     }
    185 
    186     private void setMode(final int mode) {
    187         if (mCurrentMode != mode) {
    188             mCurrentMode = mode;
    189             updateVisualState();
    190         }
    191     }
    192 
    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 
    221     public void setThemeColor(final int color) {
    222         mThemeColor = color;
    223         updateRecordButtonAppearance();
    224     }
    225 
    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
    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
    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 
    298     private Uri stopRecording() {
    299         if (mMediaRecorder.isRecording()) {
    300             return mMediaRecorder.stopRecording();
    301         }
    302         return null;
    303     }
    304 
    305     @Override   // From MediaRecorder.OnInfoListener
    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
    318     public void onError(final MediaRecorder mr, final int what, final int extra) {
    319         onErrorWhileRecording(what, extra);
    320     }
    321 
    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 
    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 
    344     private void playAudioStartSound(final OnCompletionListener completionListener) {
    345         MediaUtil.get().playSound(getContext(), R.raw.audio_initiate, completionListener);
    346     }
    347 
    348     private void playAudioEndSound() {
    349         MediaUtil.get().playSound(getContext(), R.raw.audio_end, null);
    350     }
    351 }
    352