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.media.MediaRecorder;
     19 import android.net.Uri;
     20 import android.os.ParcelFileDescriptor;
     21 
     22 import com.android.messaging.Factory;
     23 import com.android.messaging.R;
     24 import com.android.messaging.datamodel.MediaScratchFileProvider;
     25 import com.android.messaging.util.Assert;
     26 import com.android.messaging.util.ContentType;
     27 import com.android.messaging.util.LogUtil;
     28 import com.android.messaging.util.SafeAsyncTask;
     29 import com.android.messaging.util.UiUtils;
     30 
     31 import java.io.IOException;
     32 
     33 /**
     34  * Wraps around the functionalities of MediaRecorder, performs routine setup for audio recording
     35  * and updates the audio level to be displayed in UI.
     36  *
     37  * During the start and end of a recording session, we kick off a thread that polls for audio
     38  * levels, and updates the thread-safe AudioLevelSource instance. Consumers may bind to the
     39  * sound level by either polling from the level source, or register for a level change callback
     40  * on the level source object. In Bugle, the UI element (SoundLevels) polls for the sound level
     41  * on the UI thread by using animation ticks and invalidating itself.
     42  *
     43  * Aside from tracking sound levels, this also encapsulates the functionality to save the file
     44  * to the scratch space. The saved file is returned by calling stopRecording().
     45  */
     46 public class LevelTrackingMediaRecorder {
     47     // We refresh sound level every 100ms during a recording session.
     48     private static final int REFRESH_INTERVAL_MILLIS = 100;
     49 
     50     // The native amplitude returned from MediaRecorder ranges from 0~32768 (unfortunately, this
     51     // is not a constant that's defined anywhere, but the framework's Recorder app is using the
     52     // same hard-coded number). Therefore, a constant is needed in order to make it 0~100.
     53     private static final int MAX_AMPLITUDE_FACTOR = 32768 / 100;
     54 
     55     // We want to limit the max audio file size by the max message size allowed by MmsConfig,
     56     // plus multiplied by this fudge ratio to guarantee that we don't go over limit.
     57     private static final float MAX_SIZE_RATIO = 0.8f;
     58 
     59     // Default recorder settings for Bugle.
     60     // TODO: Do we want these to be tweakable?
     61     private static final int MEDIA_RECORDER_AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;
     62     private static final int MEDIA_RECORDER_OUTPUT_FORMAT = MediaRecorder.OutputFormat.THREE_GPP;
     63     private static final int MEDIA_RECORDER_AUDIO_ENCODER = MediaRecorder.AudioEncoder.AMR_NB;
     64 
     65     private final AudioLevelSource mLevelSource;
     66     private Thread mRefreshLevelThread;
     67     private MediaRecorder mRecorder;
     68     private Uri mOutputUri;
     69     private ParcelFileDescriptor mOutputFD;
     70 
     71     public LevelTrackingMediaRecorder() {
     72         mLevelSource = new AudioLevelSource();
     73     }
     74 
     75     public AudioLevelSource getLevelSource() {
     76         return mLevelSource;
     77     }
     78 
     79     /**
     80      * @return if we are currently in a recording session.
     81      */
     82     public boolean isRecording() {
     83         return mRecorder != null;
     84     }
     85 
     86     /**
     87      * Start a new recording session.
     88      * @return true if a session is successfully started; false if something went wrong or if
     89      *         we are already recording.
     90      */
     91     public boolean startRecording(final MediaRecorder.OnErrorListener errorListener,
     92             final MediaRecorder.OnInfoListener infoListener, int maxSize) {
     93         synchronized (LevelTrackingMediaRecorder.class) {
     94             if (mRecorder == null) {
     95                 mOutputUri = MediaScratchFileProvider.buildMediaScratchSpaceUri(
     96                         ContentType.THREE_GPP_EXTENSION);
     97                 mRecorder = new MediaRecorder();
     98                 try {
     99                     // The scratch space file is a Uri, however MediaRecorder
    100                     // API only accepts absolute FD's. Therefore, get the
    101                     // FileDescriptor from the content resolver to ensure the
    102                     // directory is created and get the file path to output the
    103                     // audio to.
    104                     maxSize *= MAX_SIZE_RATIO;
    105                     mOutputFD = Factory.get().getApplicationContext()
    106                             .getContentResolver().openFileDescriptor(mOutputUri, "w");
    107                     mRecorder.setAudioSource(MEDIA_RECORDER_AUDIO_SOURCE);
    108                     mRecorder.setOutputFormat(MEDIA_RECORDER_OUTPUT_FORMAT);
    109                     mRecorder.setAudioEncoder(MEDIA_RECORDER_AUDIO_ENCODER);
    110                     mRecorder.setOutputFile(mOutputFD.getFileDescriptor());
    111                     mRecorder.setMaxFileSize(maxSize);
    112                     mRecorder.setOnErrorListener(errorListener);
    113                     mRecorder.setOnInfoListener(infoListener);
    114                     mRecorder.prepare();
    115                     mRecorder.start();
    116                     startTrackingSoundLevel();
    117                     return true;
    118                 } catch (final Exception e) {
    119                     // There may be a device failure or I/O failure, record the error but
    120                     // don't fail.
    121                     LogUtil.e(LogUtil.BUGLE_TAG, "Something went wrong when starting " +
    122                             "media recorder. " + e);
    123                     UiUtils.showToastAtBottom(R.string.audio_recording_start_failed);
    124                     stopRecording();
    125                 }
    126             } else {
    127                 Assert.fail("Trying to start a new recording session while already recording!");
    128             }
    129             return false;
    130         }
    131     }
    132 
    133     /**
    134      * Stop the current recording session.
    135      * @return the Uri of the output file, or null if not currently recording.
    136      */
    137     public Uri stopRecording() {
    138         synchronized (LevelTrackingMediaRecorder.class) {
    139             if (mRecorder != null) {
    140                 try {
    141                     mRecorder.stop();
    142                 } catch (final RuntimeException ex) {
    143                     // This may happen when the recording is too short, so just drop the recording
    144                     // in this case.
    145                     LogUtil.w(LogUtil.BUGLE_TAG, "Something went wrong when stopping " +
    146                             "media recorder. " + ex);
    147                     if (mOutputUri != null) {
    148                         final Uri outputUri = mOutputUri;
    149                         SafeAsyncTask.executeOnThreadPool(new Runnable() {
    150                             @Override
    151                             public void run() {
    152                                 Factory.get().getApplicationContext().getContentResolver().delete(
    153                                         outputUri, null, null);
    154                             }
    155                         });
    156                         mOutputUri = null;
    157                     }
    158                 } finally {
    159                     mRecorder.release();
    160                     mRecorder = null;
    161                 }
    162             } else {
    163                 Assert.fail("Not currently recording!");
    164                 return null;
    165             }
    166         }
    167 
    168         if (mOutputFD != null) {
    169             try {
    170                 mOutputFD.close();
    171             } catch (final IOException e) {
    172                 // Nothing to do
    173             }
    174             mOutputFD = null;
    175         }
    176 
    177         stopTrackingSoundLevel();
    178         return mOutputUri;
    179     }
    180 
    181     private int getAmplitude() {
    182         synchronized (LevelTrackingMediaRecorder.class) {
    183             if (mRecorder != null) {
    184                 final int maxAmplitude = mRecorder.getMaxAmplitude() / MAX_AMPLITUDE_FACTOR;
    185                 return Math.min(maxAmplitude, 100);
    186             } else {
    187                 return 0;
    188             }
    189         }
    190     }
    191 
    192     private void startTrackingSoundLevel() {
    193         stopTrackingSoundLevel();
    194         mRefreshLevelThread = new Thread() {
    195             @Override
    196             public void run() {
    197                 try {
    198                     while (true) {
    199                         synchronized (LevelTrackingMediaRecorder.class) {
    200                             if (mRecorder != null) {
    201                                 mLevelSource.setSpeechLevel(getAmplitude());
    202                             } else {
    203                                 // The recording session is over, finish the thread.
    204                                 return;
    205                             }
    206                         }
    207                         Thread.sleep(REFRESH_INTERVAL_MILLIS);
    208                     }
    209                 } catch (final InterruptedException e) {
    210                     Thread.currentThread().interrupt();
    211                 }
    212             }
    213         };
    214         mRefreshLevelThread.start();
    215     }
    216 
    217     private void stopTrackingSoundLevel() {
    218         if (mRefreshLevelThread != null && mRefreshLevelThread.isAlive()) {
    219             mRefreshLevelThread.interrupt();
    220             mRefreshLevelThread = null;
    221         }
    222     }
    223 }
    224