LevelTrackingMediaRecorder.java revision 461a34b466cb4b13dbbc2ec6330b31e217b2ac4e
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 */
16package com.android.messaging.ui.mediapicker;
17
18import android.media.MediaRecorder;
19import android.net.Uri;
20import android.os.ParcelFileDescriptor;
21
22import com.android.messaging.Factory;
23import com.android.messaging.R;
24import com.android.messaging.datamodel.MediaScratchFileProvider;
25import com.android.messaging.util.Assert;
26import com.android.messaging.util.ContentType;
27import com.android.messaging.util.LogUtil;
28import com.android.messaging.util.SafeAsyncTask;
29import com.android.messaging.util.UiUtils;
30
31import 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 */
46public 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