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 
LevelTrackingMediaRecorder()71     public LevelTrackingMediaRecorder() {
72         mLevelSource = new AudioLevelSource();
73     }
74 
getLevelSource()75     public AudioLevelSource getLevelSource() {
76         return mLevelSource;
77     }
78 
79     /**
80      * @return if we are currently in a recording session.
81      */
isRecording()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      */
startRecording(final MediaRecorder.OnErrorListener errorListener, final MediaRecorder.OnInfoListener infoListener, int maxSize)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      */
stopRecording()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 
getAmplitude()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 
startTrackingSoundLevel()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 
stopTrackingSoundLevel()217     private void stopTrackingSoundLevel() {
218         if (mRefreshLevelThread != null && mRefreshLevelThread.isAlive()) {
219             mRefreshLevelThread.interrupt();
220             mRefreshLevelThread = null;
221         }
222     }
223 }
224