summaryrefslogtreecommitdiffstats
path: root/src/com/android/messaging/ui/mediapicker/LevelTrackingMediaRecorder.java
blob: 06730a3bfb63b4e4cbc5af17552525e05049b083 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.messaging.ui.mediapicker;

import android.media.MediaRecorder;
import android.net.Uri;
import android.os.ParcelFileDescriptor;

import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.datamodel.MediaScratchFileProvider;
import com.android.messaging.util.Assert;
import com.android.messaging.util.ContentType;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.SafeAsyncTask;
import com.android.messaging.util.UiUtils;

import java.io.IOException;

/**
 * Wraps around the functionalities of MediaRecorder, performs routine setup for audio recording
 * and updates the audio level to be displayed in UI.
 *
 * During the start and end of a recording session, we kick off a thread that polls for audio
 * levels, and updates the thread-safe AudioLevelSource instance. Consumers may bind to the
 * sound level by either polling from the level source, or register for a level change callback
 * on the level source object. In Bugle, the UI element (SoundLevels) polls for the sound level
 * on the UI thread by using animation ticks and invalidating itself.
 *
 * Aside from tracking sound levels, this also encapsulates the functionality to save the file
 * to the scratch space. The saved file is returned by calling stopRecording().
 */
public class LevelTrackingMediaRecorder {
    // We refresh sound level every 100ms during a recording session.
    private static final int REFRESH_INTERVAL_MILLIS = 100;

    // The native amplitude returned from MediaRecorder ranges from 0~32768 (unfortunately, this
    // is not a constant that's defined anywhere, but the framework's Recorder app is using the
    // same hard-coded number). Therefore, a constant is needed in order to make it 0~100.
    private static final int MAX_AMPLITUDE_FACTOR = 32768 / 100;

    // We want to limit the max audio file size by the max message size allowed by MmsConfig,
    // plus multiplied by this fudge ratio to guarantee that we don't go over limit.
    private static final float MAX_SIZE_RATIO = 0.8f;

    // Default recorder settings for Bugle.
    // TODO: Do we want these to be tweakable?
    private static final int MEDIA_RECORDER_AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;
    private static final int MEDIA_RECORDER_OUTPUT_FORMAT = MediaRecorder.OutputFormat.THREE_GPP;
    private static final int MEDIA_RECORDER_AUDIO_ENCODER = MediaRecorder.AudioEncoder.AMR_NB;

    private final AudioLevelSource mLevelSource;
    private Thread mRefreshLevelThread;
    private MediaRecorder mRecorder;
    private Uri mOutputUri;
    private ParcelFileDescriptor mOutputFD;

    public LevelTrackingMediaRecorder() {
        mLevelSource = new AudioLevelSource();
    }

    public AudioLevelSource getLevelSource() {
        return mLevelSource;
    }

    /**
     * @return if we are currently in a recording session.
     */
    public boolean isRecording() {
        return mRecorder != null;
    }

    /**
     * Start a new recording session.
     * @return true if a session is successfully started; false if something went wrong or if
     *         we are already recording.
     */
    public boolean startRecording(final MediaRecorder.OnErrorListener errorListener,
            final MediaRecorder.OnInfoListener infoListener, int maxSize) {
        synchronized (LevelTrackingMediaRecorder.class) {
            if (mRecorder == null) {
                mOutputUri = MediaScratchFileProvider.buildMediaScratchSpaceUri(
                        ContentType.THREE_GPP_EXTENSION);
                mRecorder = new MediaRecorder();
                try {
                    // The scratch space file is a Uri, however MediaRecorder
                    // API only accepts absolute FD's. Therefore, get the
                    // FileDescriptor from the content resolver to ensure the
                    // directory is created and get the file path to output the
                    // audio to.
                    maxSize *= MAX_SIZE_RATIO;
                    mOutputFD = Factory.get().getApplicationContext()
                            .getContentResolver().openFileDescriptor(mOutputUri, "w");
                    mRecorder.setAudioSource(MEDIA_RECORDER_AUDIO_SOURCE);
                    mRecorder.setOutputFormat(MEDIA_RECORDER_OUTPUT_FORMAT);
                    mRecorder.setAudioEncoder(MEDIA_RECORDER_AUDIO_ENCODER);
                    mRecorder.setOutputFile(mOutputFD.getFileDescriptor());
                    mRecorder.setMaxFileSize(maxSize);
                    mRecorder.setOnErrorListener(errorListener);
                    mRecorder.setOnInfoListener(infoListener);
                    mRecorder.prepare();
                    mRecorder.start();
                    startTrackingSoundLevel();
                    return true;
                } catch (final Exception e) {
                    // There may be a device failure or I/O failure, record the error but
                    // don't fail.
                    LogUtil.e(LogUtil.BUGLE_TAG, "Something went wrong when starting " +
                            "media recorder. " + e);
                    UiUtils.showToastAtBottom(R.string.audio_recording_start_failed);
                    stopRecording();
                }
            } else {
                Assert.fail("Trying to start a new recording session while already recording!");
            }
            return false;
        }
    }

    /**
     * Stop the current recording session.
     * @return the Uri of the output file, or null if not currently recording.
     */
    public Uri stopRecording() {
        synchronized (LevelTrackingMediaRecorder.class) {
            if (mRecorder != null) {
                try {
                    mRecorder.stop();
                } catch (final RuntimeException ex) {
                    // This may happen when the recording is too short, so just drop the recording
                    // in this case.
                    LogUtil.w(LogUtil.BUGLE_TAG, "Something went wrong when stopping " +
                            "media recorder. " + ex);
                    if (mOutputUri != null) {
                        final Uri outputUri = mOutputUri;
                        SafeAsyncTask.executeOnThreadPool(new Runnable() {
                            @Override
                            public void run() {
                                Factory.get().getApplicationContext().getContentResolver().delete(
                                        outputUri, null, null);
                            }
                        });
                        mOutputUri = null;
                    }
                } finally {
                    mRecorder.release();
                    mRecorder = null;
                }
            } else {
                Assert.fail("Not currently recording!");
                return null;
            }
        }

        if (mOutputFD != null) {
            try {
                mOutputFD.close();
            } catch (final IOException e) {
                // Nothing to do
            }
            mOutputFD = null;
        }

        stopTrackingSoundLevel();
        return mOutputUri;
    }

    private int getAmplitude() {
        synchronized (LevelTrackingMediaRecorder.class) {
            if (mRecorder != null) {
                final int maxAmplitude = mRecorder.getMaxAmplitude() / MAX_AMPLITUDE_FACTOR;
                return Math.min(maxAmplitude, 100);
            } else {
                return 0;
            }
        }
    }

    private void startTrackingSoundLevel() {
        stopTrackingSoundLevel();
        mRefreshLevelThread = new Thread() {
            @Override
            public void run() {
                try {
                    while (true) {
                        synchronized (LevelTrackingMediaRecorder.class) {
                            if (mRecorder != null) {
                                mLevelSource.setSpeechLevel(getAmplitude());
                            } else {
                                // The recording session is over, finish the thread.
                                return;
                            }
                        }
                        Thread.sleep(REFRESH_INTERVAL_MILLIS);
                    }
                } catch (final InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        };
        mRefreshLevelThread.start();
    }

    private void stopTrackingSoundLevel() {
        if (mRefreshLevelThread != null && mRefreshLevelThread.isAlive()) {
            mRefreshLevelThread.interrupt();
            mRefreshLevelThread = null;
        }
    }
}