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