AI2 Component  (Version nb184)
SoundRecorder.java
Go to the documentation of this file.
1 // -*- mode: java; c-basic-offset: 2; -*-
2 // Copyright 2009-2011 Google, All Rights reserved
3 // Copyright 2011-2020 MIT, All rights reserved
4 // Released under the Apache License, Version 2.0
5 // http://www.apache.org/licenses/LICENSE-2.0
6 
7 package com.google.appinventor.components.runtime;
8 
24 import android.Manifest;
25 import android.media.MediaRecorder;
26 import android.media.MediaRecorder.OnErrorListener;
27 import android.media.MediaRecorder.OnInfoListener;
28 import android.os.Environment;
29 import android.util.Log;
30 
31 import java.io.IOException;
32 
39 @DesignerComponent(version = YaVersion.SOUND_RECORDER_COMPONENT_VERSION,
40  description = "<p>Multimedia component that records audio.</p>",
41  category = ComponentCategory.MEDIA,
42  nonVisible = true,
43  iconName = "images/soundRecorder.png")
44 @SimpleObject
45 @UsesPermissions(permissionNames = "android.permission.RECORD_AUDIO," +
46  "android.permission.WRITE_EXTERNAL_STORAGE," +
47  "android.permission.READ_EXTERNAL_STORAGE")
48 public final class SoundRecorder extends AndroidNonvisibleComponent
49  implements Component, OnErrorListener, OnInfoListener {
50 
51  private static final String TAG = "SoundRecorder";
52 
53  // the path to the savedRecording
54  // if it is the null string, the recorder will generate a path
55  // note that this is also initialized to "" in the designer
56  private String savedRecording = "";
57 
58  // Whether or not we have the RECORD_AUDIO permission
59  private boolean havePermission = false;
60 
64  private class RecordingController {
65  final MediaRecorder recorder;
66 
67  // file is the same as savedRecording, but we'll keep it local to the
68  // RecordingController for future flexibility
69  final String file;
70 
71  RecordingController(String savedRecording) throws IOException {
72  // pick a pathname if none was specified
73  file = (savedRecording.equals("")) ?
74  FileUtil.getRecordingFile(form, "3gp").getAbsolutePath() :
75  savedRecording;
76 
77  recorder = new MediaRecorder();
78  recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
79  recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
80  recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
81  Log.i(TAG, "Setting output file to " + file);
82  recorder.setOutputFile(file);
83  Log.i(TAG, "preparing");
84  recorder.prepare();
85  recorder.setOnErrorListener(SoundRecorder.this);
86  recorder.setOnInfoListener(SoundRecorder.this);
87  }
88 
89  void start() throws IllegalStateException {
90  Log.i(TAG, "starting");
91 
92  try {
93  recorder.start();
94  } catch (IllegalStateException e) {
95  // This is the error produced when there are two recorders running.
96  // There might be other causes, but we don't know them.
97  // Using Log.e will log a stack trace, so we can investigate
98  Log.e(TAG, "got IllegalStateException. Are there two recorders running?", e);
99  // Pass back a message detail for dispatchErrorOccurred to
100  // show at user level
101  throw (new IllegalStateException("Is there another recording running?"));
102  }
103  }
104 
105  void stop() {
106  recorder.setOnErrorListener(null);
107  recorder.setOnInfoListener(null);
108  recorder.stop();
109  recorder.reset();
110  recorder.release();
111  }
112  }
113 
114  /*
115  * This is null when not recording, and contains the active RecordingState
116  * when recording.
117  */
118  private RecordingController controller;
119 
120  public SoundRecorder(final ComponentContainer container) {
121  super(container.$form());
122  }
123 
124 
134  description = "Specifies the path to the file where the recording should be stored. " +
135  "If this property is the empty string, then starting a recording will create a file in " +
136  "an appropriate location. If the property is not the empty string, it should specify " +
137  "a complete path to a file in an existing directory, including a file name with the " +
138  "extension .3gp." ,
139  category = PropertyCategory.BEHAVIOR)
140  public String SavedRecording() {
141  return savedRecording;
142  }
143 
151  defaultValue = "")
153  public void SavedRecording(String pathName) {
154  savedRecording = pathName;
155  }
156 
161  public void Start() {
162  // Need to check if we have RECORD_AUDIO and WRITE_EXTERNAL permissions
163  if (!havePermission) {
164  final SoundRecorder me = this;
165  form.runOnUiThread(new Runnable() {
166  @Override
167  public void run() {
168  form.askPermission(new BulkPermissionRequest(me, "Start",
169  Manifest.permission.RECORD_AUDIO, Manifest.permission.WRITE_EXTERNAL_STORAGE) {
170  @Override
171  public void onGranted() {
172  me.havePermission = true;
173  me.Start();
174  }
175  });
176  }
177  });
178  return;
179  }
180 
181  if (controller != null) {
182  Log.i(TAG, "Start() called, but already recording to " + controller.file);
183  return;
184  }
185  Log.i(TAG, "Start() called");
186  if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
187  form.dispatchErrorOccurredEvent(
188  this, "Start", ErrorMessages.ERROR_MEDIA_EXTERNAL_STORAGE_NOT_AVAILABLE);
189  return;
190  }
191  try {
192  controller = new RecordingController(savedRecording);
193  } catch (PermissionException e) {
194  form.dispatchPermissionDeniedEvent(this, "Start", e);
195  return;
196  } catch (Throwable t) {
197  form.dispatchErrorOccurredEvent(
198  this, "Start", ErrorMessages.ERROR_SOUND_RECORDER_CANNOT_CREATE, t.getMessage());
199  return;
200  }
201  try {
202  controller.start();
203  } catch (Throwable t) {
204  // I'm commenting the next line out because stop can throw an error, and
205  // it's not clear to me how to handle that.
206  // controller.stop();
207  controller = null;
208  form.dispatchErrorOccurredEvent(
209  this, "Start", ErrorMessages.ERROR_SOUND_RECORDER_CANNOT_CREATE, t.getMessage());
210  return;
211  }
212  StartedRecording();
213  }
214 
215  @Override
216  public void onError(MediaRecorder affectedRecorder, int what, int extra) {
217  if (controller == null || affectedRecorder != controller.recorder) {
218  Log.w(TAG, "onError called with wrong recorder. Ignoring.");
219  return;
220  }
221  form.dispatchErrorOccurredEvent(this, "onError", ErrorMessages.ERROR_SOUND_RECORDER);
222  try {
223  controller.stop();
224  } catch (Throwable e) {
225  Log.w(TAG, e.getMessage());
226  } finally {
227  controller = null;
228  StoppedRecording();
229  }
230  }
231 
232  @Override
233  public void onInfo(MediaRecorder affectedRecorder, int what, int extra) {
234  if (controller == null || affectedRecorder != controller.recorder) {
235  Log.w(TAG, "onInfo called with wrong recorder. Ignoring.");
236  return;
237  }
238  switch (what) {
239  case MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED:
240  form.dispatchErrorOccurredEvent(this, "recording",
242  break;
243  case MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED:
244  form.dispatchErrorOccurredEvent(this, "recording",
246  break;
247  case MediaRecorder.MEDIA_RECORDER_INFO_UNKNOWN:
248  form.dispatchErrorOccurredEvent(this, "recording", ErrorMessages.ERROR_SOUND_RECORDER);
249  break;
250  default:
251  // value of `what` is not valid, probably device-specific debugging. escape early to prevent
252  // stoppage until we see an Android-defined error. See also:
253  // http://stackoverflow.com/questions/25785420/mediarecorder-oninfolistener-giving-an-895
254  return;
255  }
256  try {
257  Log.i(TAG, "Recoverable condition while recording. Will attempt to stop normally.");
258  controller.recorder.stop();
259  } catch(IllegalStateException e) {
260  Log.i(TAG, "SoundRecorder was not in a recording state.", e);
261  form.dispatchErrorOccurredEventDialog(this, "Stop",
263  } finally {
264  controller = null;
265  StoppedRecording();
266  }
267  }
268 
273  public void Stop() {
274  if (controller == null) {
275  Log.i(TAG, "Stop() called, but already stopped.");
276  return;
277  }
278  try {
279  Log.i(TAG, "Stop() called");
280  Log.i(TAG, "stopping");
281  controller.stop();
282  Log.i(TAG, "Firing AfterSoundRecorded with " + controller.file);
283  AfterSoundRecorded(controller.file);
284  } catch (Throwable t) {
285  form.dispatchErrorOccurredEvent(this, "Stop", ErrorMessages.ERROR_SOUND_RECORDER);
286  } finally {
287  controller = null;
288  StoppedRecording();
289  }
290  }
291 
292  @SimpleEvent(description = "Provides the location of the newly created sound.")
293  public void AfterSoundRecorded(final String sound) {
294  EventDispatcher.dispatchEvent(this, "AfterSoundRecorded", sound);
295  }
296 
297  @SimpleEvent(description = "Indicates that the recorder has started, and can be stopped.")
298  public void StartedRecording() {
299  EventDispatcher.dispatchEvent(this, "StartedRecording");
300  }
301 
302  @SimpleEvent(description = "Indicates that the recorder has stopped, and can be started again.")
303  public void StoppedRecording() {
304  EventDispatcher.dispatchEvent(this, "StoppedRecording");
305  }
306 }
com.google.appinventor.components.runtime.EventDispatcher
Definition: EventDispatcher.java:22
com.google.appinventor.components.runtime.util.ErrorMessages.ERROR_SOUND_RECORDER_MAX_FILESIZE_REACHED
static final int ERROR_SOUND_RECORDER_MAX_FILESIZE_REACHED
Definition: ErrorMessages.java:110
com.google.appinventor.components.annotations.SimpleFunction
Definition: SimpleFunction.java:23
com.google.appinventor.components.runtime.SoundRecorder.onError
void onError(MediaRecorder affectedRecorder, int what, int extra)
Definition: SoundRecorder.java:216
com.google.appinventor.components.runtime.util.ErrorMessages
Definition: ErrorMessages.java:17
com.google.appinventor.components.runtime.util
-*- mode: java; c-basic-offset: 2; -*-
Definition: AccountChooser.java:7
com.google.appinventor.components.runtime.util.FileUtil
Definition: FileUtil.java:37
com.google.appinventor.components.common.YaVersion
Definition: YaVersion.java:14
com.google.appinventor.components.annotations.DesignerProperty
Definition: DesignerProperty.java:25
com.google.appinventor.components.runtime.util.FileUtil.getRecordingFile
static File getRecordingFile(String extension)
Definition: FileUtil.java:417
com.google.appinventor.components.common.PropertyTypeConstants.PROPERTY_TYPE_STRING
static final String PROPERTY_TYPE_STRING
Definition: PropertyTypeConstants.java:237
com.google.appinventor.components.runtime.SoundRecorder.SoundRecorder
SoundRecorder(final ComponentContainer container)
Definition: SoundRecorder.java:120
com.google.appinventor.components
com.google.appinventor.components.annotations.DesignerComponent
Definition: DesignerComponent.java:22
com.google.appinventor.components.annotations.SimpleEvent
Definition: SimpleEvent.java:20
com.google.appinventor.components.annotations.PropertyCategory.BEHAVIOR
BEHAVIOR
Definition: PropertyCategory.java:15
com.google.appinventor.components.runtime.SoundRecorder.Stop
void Stop()
Definition: SoundRecorder.java:273
com.google.appinventor.components.runtime.util.ErrorMessages.ERROR_SOUND_RECORDER_MAX_DURATION_REACHED
static final int ERROR_SOUND_RECORDER_MAX_DURATION_REACHED
Definition: ErrorMessages.java:109
com.google.appinventor.components.runtime.util.ErrorMessages.ERROR_SOUND_RECORDER_ILLEGAL_STOP
static final int ERROR_SOUND_RECORDER_ILLEGAL_STOP
Definition: ErrorMessages.java:108
com.google.appinventor.components.annotations.UsesPermissions
Definition: UsesPermissions.java:21
com.google.appinventor.components.runtime.EventDispatcher.dispatchEvent
static boolean dispatchEvent(Component component, String eventName, Object...args)
Definition: EventDispatcher.java:188
com.google.appinventor.components.runtime.AndroidNonvisibleComponent
Definition: AndroidNonvisibleComponent.java:17
com.google.appinventor.components.annotations.SimpleProperty
Definition: SimpleProperty.java:23
com.google.appinventor.components.runtime.util.BulkPermissionRequest
Definition: BulkPermissionRequest.java:22
com.google.appinventor.components.annotations.PropertyCategory
Definition: PropertyCategory.java:13
com.google.appinventor.components.runtime.errors.PermissionException
Definition: PermissionException.java:16
com.google.appinventor.components.runtime.ComponentContainer
Definition: ComponentContainer.java:16
com.google.appinventor.components.runtime
Copyright 2009-2011 Google, All Rights reserved.
Definition: AccelerometerSensor.java:8
com.google.appinventor.components.runtime.Component
Definition: Component.java:17
com.google.appinventor.components.runtime.SoundRecorder
Definition: SoundRecorder.java:48
com.google.appinventor.components.common
Definition: ComponentCategory.java:7
com.google.appinventor.components.common.ComponentCategory
Definition: ComponentCategory.java:48
com.google.appinventor.components.runtime.SoundRecorder.SavedRecording
void SavedRecording(String pathName)
Definition: SoundRecorder.java:153
com.google.appinventor.components.annotations.SimpleObject
Definition: SimpleObject.java:23
com.google
com
com.google.appinventor.components.runtime.errors
Definition: ArrayIndexOutOfBoundsError.java:7
com.google.appinventor.components.runtime.ComponentContainer.$form
Form $form()
com.google.appinventor.components.common.PropertyTypeConstants
Definition: PropertyTypeConstants.java:14
com.google.appinventor.components.runtime.SoundRecorder.Start
void Start()
Definition: SoundRecorder.java:161
com.google.appinventor.components.annotations
com.google.appinventor.components.runtime.SoundRecorder.onInfo
void onInfo(MediaRecorder affectedRecorder, int what, int extra)
Definition: SoundRecorder.java:233
com.google.appinventor
com.google.appinventor.components.runtime.util.ErrorMessages.ERROR_SOUND_RECORDER
static final int ERROR_SOUND_RECORDER
Definition: ErrorMessages.java:106