blob: 18a9d0006d9b13f8672ce6fa56ea285aca06fd8c [file] [log] [blame]
Benson Huanga8b6afc2014-11-20 15:42:26 +08001/*
2 * Copyright (C) 2014 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
17package com.android.fmradio;
18
19import android.content.ContentResolver;
20import android.content.ContentUris;
21import android.content.ContentValues;
22import android.content.Context;
23import android.database.Cursor;
24import android.media.MediaPlayer;
25import android.media.MediaRecorder;
26import android.media.MediaScannerConnection;
27import android.net.Uri;
28import android.os.Environment;
29import android.os.SystemClock;
30import android.provider.MediaStore;
31import android.text.format.DateFormat;
32import android.util.Log;
33
34import java.io.File;
35import java.io.IOException;
36import java.text.SimpleDateFormat;
37import java.util.Date;
38import java.util.Locale;
39
40/**
41 * This class provider interface to recording, stop recording, save recording
42 * file, play recording file
43 */
44public class FmRecorder implements MediaRecorder.OnErrorListener, MediaRecorder.OnInfoListener {
45 private static final String TAG = "FmRecorder";
46 // file prefix
47 public static final String RECORDING_FILE_PREFIX = "FM";
48 // file extension
49 public static final String RECORDING_FILE_EXTENSION = ".3gpp";
50 // recording file folder
51 public static final String FM_RECORD_FOLDER = "FM Recording";
52 private static final String RECORDING_FILE_TYPE = "audio/3gpp";
53 private static final String RECORDING_FILE_SOURCE = "FM Recordings";
54 // error type no sdcard
55 public static final int ERROR_SDCARD_NOT_PRESENT = 0;
56 // error type sdcard not have enough space
57 public static final int ERROR_SDCARD_INSUFFICIENT_SPACE = 1;
58 // error type can't write sdcard
59 public static final int ERROR_SDCARD_WRITE_FAILED = 2;
60 // error type recorder internal error occur
61 public static final int ERROR_RECORDER_INTERNAL = 3;
62
63 // FM Recorder state not recording and not playing
64 public static final int STATE_IDLE = 5;
65 // FM Recorder state recording
66 public static final int STATE_RECORDING = 6;
67 // FM Recorder state playing
68 public static final int STATE_PLAYBACK = 7;
69 // FM Recorder state invalid, need to check
70 public static final int STATE_INVALID = -1;
71
72 // use to record current FM recorder state
73 public int mInternalState = STATE_IDLE;
74 // the recording time after start recording
75 private long mRecordTime = 0;
76 // record start time
77 private long mRecordStartTime = 0;
78 // current record file
79 private File mRecordFile = null;
80 // record current record file is saved by user
81 private boolean mIsRecordingFileSaved = false;
82 // listener use for notify service the record state or error state
83 private OnRecorderStateChangedListener mStateListener = null;
84 // recorder use for record file
85 private MediaRecorder mRecorder = null;
86
87 /**
88 * Start recording the voice of FM, also check the pre-conditions, if not
89 * meet, will return an error message to the caller. if can start recording
90 * success, will set FM record state to recording and notify to the caller
91 */
92 public void startRecording(Context context) {
93 mRecordTime = 0;
94
95 // Check external storage
96 if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
97 Log.e(TAG, "startRecording, no external storage available");
98 setError(ERROR_SDCARD_NOT_PRESENT);
99 return;
100 }
101
102 String recordingSdcard = FmUtils.getDefaultStoragePath();
103 // check whether have sufficient storage space, if not will notify
104 // caller error message
105 if (!FmUtils.hasEnoughSpace(recordingSdcard)) {
106 setError(ERROR_SDCARD_INSUFFICIENT_SPACE);
107 Log.e(TAG, "startRecording, SD card does not have sufficient space!!");
108 return;
109 }
110
111 // get external storage directory
112 File sdDir = new File(recordingSdcard);
113 File recordingDir = new File(sdDir, FM_RECORD_FOLDER);
114 // exist a file named FM Recording, so can't create FM recording folder
115 if (recordingDir.exists() && !recordingDir.isDirectory()) {
116 Log.e(TAG, "startRecording, a file with name \"FM Recording\" already exists!!");
117 setError(ERROR_SDCARD_WRITE_FAILED);
118 return;
119 } else if (!recordingDir.exists()) { // try to create recording folder
120 boolean mkdirResult = recordingDir.mkdir();
121 if (!mkdirResult) { // create recording file failed
122 setError(ERROR_RECORDER_INTERNAL);
123 return;
124 }
125 }
126 // create recording temporary file
127 long curTime = System.currentTimeMillis();
128 Date date = new Date(curTime);
129 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("MMddyyyy_HHmmss",
130 Locale.ENGLISH);
131 String time = simpleDateFormat.format(date);
132 StringBuilder stringBuilder = new StringBuilder();
133 stringBuilder.append(time).append(RECORDING_FILE_EXTENSION);
134 String name = stringBuilder.toString();
135 mRecordFile = new File(recordingDir, name);
136 try {
137 if (mRecordFile.createNewFile()) {
138 Log.d(TAG, "startRecording, createNewFile success with path "
139 + mRecordFile.getPath());
140 }
141 } catch (IOException e) {
142 Log.e(TAG, "startRecording, IOException while createTempFile: " + e);
143 e.printStackTrace();
144 setError(ERROR_SDCARD_WRITE_FAILED);
145 return;
146 }
147 // set record parameter and start recording
148 try {
149 mRecorder = new MediaRecorder();
150 mRecorder.setOnErrorListener(this);
151 mRecorder.setOnInfoListener(this);
Eric Laurent1df57802015-03-09 13:08:03 -0700152 mRecorder.setAudioSource(MediaRecorder.AudioSource.RADIO_TUNER);
Benson Huanga8b6afc2014-11-20 15:42:26 +0800153 mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
154 mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
155 final int samplingRate = 44100;
156 mRecorder.setAudioSamplingRate(samplingRate);
157 final int bitRate = 128000;
158 mRecorder.setAudioEncodingBitRate(bitRate);
159 final int audiochannels = 2;
160 mRecorder.setAudioChannels(audiochannels);
161 mRecorder.setOutputFile(mRecordFile.getAbsolutePath());
162 mRecorder.prepare();
163 mRecordStartTime = SystemClock.elapsedRealtime();
164 mRecorder.start();
165 mIsRecordingFileSaved = false;
166 } catch (IllegalStateException e) {
167 Log.e(TAG, "startRecording, IllegalStateException while starting recording!", e);
168 setError(ERROR_RECORDER_INTERNAL);
169 return;
170 } catch (IOException e) {
171 Log.e(TAG, "startRecording, IOException while starting recording!", e);
172 setError(ERROR_RECORDER_INTERNAL);
173 return;
174 }
175 setState(STATE_RECORDING);
176 }
177
178 /**
179 * Stop recording, compute recording time and update FM recorder state
180 */
181 public void stopRecording() {
182 if (STATE_RECORDING != mInternalState) {
183 Log.w(TAG, "stopRecording, called in wrong state!!");
184 return;
185 }
186
187 mRecordTime = SystemClock.elapsedRealtime() - mRecordStartTime;
188 stopRecorder();
189 setState(STATE_IDLE);
190 }
191
192 /**
193 * Compute the current record time
194 *
195 * @return The current record time
196 */
197 public long getRecordTime() {
198 if (STATE_RECORDING == mInternalState) {
199 mRecordTime = SystemClock.elapsedRealtime() - mRecordStartTime;
200 }
201 return mRecordTime;
202 }
203
204 /**
205 * Get FM recorder current state
206 *
207 * @return FM recorder current state
208 */
209 public int getState() {
210 return mInternalState;
211 }
212
213 /**
214 * Get current record file name
215 *
216 * @return The current record file name
217 */
218 public String getRecordFileName() {
219 if (mRecordFile != null) {
220 String fileName = mRecordFile.getName();
221 int index = fileName.indexOf(RECORDING_FILE_EXTENSION);
222 if (index > 0) {
223 fileName = fileName.substring(0, index);
224 }
225 return fileName;
226 }
227 return null;
228 }
229
230 /**
231 * Save recording file with the given name, and insert it's info to database
232 *
233 * @param context The context
234 * @param newName The name to override default recording name
235 */
236 public void saveRecording(Context context, String newName) {
237 if (mRecordFile == null) {
238 Log.e(TAG, "saveRecording, recording file is null!");
239 return;
240 }
241
242 File newRecordFile = new File(mRecordFile.getParent(), newName + RECORDING_FILE_EXTENSION);
243 boolean succuss = mRecordFile.renameTo(newRecordFile);
244 if (succuss) {
245 mRecordFile = newRecordFile;
246 }
247 mIsRecordingFileSaved = true;
248 // insert recording file info to database
249 addRecordingToDatabase(context);
250 }
251
252 /**
253 * Discard current recording file, release recorder and player
254 */
255 public void discardRecording() {
256 if ((STATE_RECORDING == mInternalState) && (null != mRecorder)) {
257 stopRecorder();
258 }
259
260 if (mRecordFile != null && !mIsRecordingFileSaved) {
261 if (!mRecordFile.delete()) {
262 // deletion failed, possibly due to hot plug out SD card
263 Log.d(TAG, "discardRecording, delete file failed!");
264 }
265 mRecordFile = null;
266 mRecordStartTime = 0;
267 mRecordTime = 0;
268 }
269 setState(STATE_IDLE);
270 }
271
272 /**
273 * Set the callback use to notify FM recorder state and error message
274 *
275 * @param listener the callback
276 */
277 public void registerRecorderStateListener(OnRecorderStateChangedListener listener) {
278 mStateListener = listener;
279 }
280
281 /**
282 * Interface to notify FM recorder state and error message
283 */
284 public interface OnRecorderStateChangedListener {
285 /**
286 * notify FM recorder state
287 *
288 * @param state current FM recorder state
289 */
290 void onRecorderStateChanged(int state);
291
292 /**
293 * notify FM recorder error message
294 *
295 * @param error error type
296 */
297 void onRecorderError(int error);
298 }
299
300 /**
301 * When recorder occur error, release player, notify error message, and
302 * update FM recorder state to idle
303 *
304 * @param mr The current recorder
305 * @param what The error message type
306 * @param extra The error message extra
307 */
308 @Override
309 public void onError(MediaRecorder mr, int what, int extra) {
310 Log.e(TAG, "onError, what = " + what + ", extra = " + extra);
311 stopRecorder();
312 setError(ERROR_RECORDER_INTERNAL);
313 if (STATE_RECORDING == mInternalState) {
314 setState(STATE_IDLE);
315 }
316 }
317
318 @Override
319 public void onInfo(MediaRecorder mr, int what, int extra) {
320 Log.d(TAG, "onInfo: what=" + what + ", extra=" + extra);
321 if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED ||
322 what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
323 onError(mr, what, extra);
324 }
325 }
326
327 /**
328 * Reset FM recorder
329 */
330 public void resetRecorder() {
331 if (mRecorder != null) {
332 mRecorder.release();
333 mRecorder = null;
334 }
335 mRecordFile = null;
336 mRecordStartTime = 0;
337 mRecordTime = 0;
338 mInternalState = STATE_IDLE;
339 }
340
341 /**
342 * Notify error message to the callback
343 *
344 * @param error FM recorder error type
345 */
346 private void setError(int error) {
347 if (mStateListener != null) {
348 mStateListener.onRecorderError(error);
349 }
350 }
351
352 /**
353 * Notify FM recorder state message to the callback
354 *
355 * @param state FM recorder current state
356 */
357 private void setState(int state) {
358 mInternalState = state;
359 if (mStateListener != null) {
360 mStateListener.onRecorderStateChanged(state);
361 }
362 }
363
364 /**
365 * Save recording file info to database
366 *
367 * @param context The context
368 */
369 private void addRecordingToDatabase(final Context context) {
370 long curTime = System.currentTimeMillis();
371 long modDate = mRecordFile.lastModified();
372 Date date = new Date(curTime);
373
374 java.text.DateFormat dateFormatter = DateFormat.getDateFormat(context);
375 java.text.DateFormat timeFormatter = DateFormat.getTimeFormat(context);
376 String title = getRecordFileName();
377 StringBuilder stringBuilder = new StringBuilder()
378 .append(FM_RECORD_FOLDER)
379 .append(" ")
380 .append(dateFormatter.format(date))
381 .append(" ")
382 .append(timeFormatter.format(date));
383 String artist = stringBuilder.toString();
384
385 final int size = 9;
386 ContentValues cv = new ContentValues(size);
387 cv.put(MediaStore.Audio.Media.IS_MUSIC, 1);
388 cv.put(MediaStore.Audio.Media.TITLE, title);
389 cv.put(MediaStore.Audio.Media.DATA, mRecordFile.getAbsolutePath());
390 final int oneSecond = 1000;
391 cv.put(MediaStore.Audio.Media.DATE_ADDED, (int) (curTime / oneSecond));
392 cv.put(MediaStore.Audio.Media.DATE_MODIFIED, (int) (modDate / oneSecond));
393 cv.put(MediaStore.Audio.Media.MIME_TYPE, RECORDING_FILE_TYPE);
394 cv.put(MediaStore.Audio.Media.ARTIST, artist);
395 cv.put(MediaStore.Audio.Media.ALBUM, RECORDING_FILE_SOURCE);
396 cv.put(MediaStore.Audio.Media.DURATION, mRecordTime);
397
398 int recordingId = addToAudioTable(context, cv);
399 if (recordingId < 0) {
400 // insert failed
401 return;
402 }
403 int playlistId = getPlaylistId(context);
404 if (playlistId < 0) {
405 // play list not exist, create FM Recording play list
406 playlistId = createPlaylist(context);
407 }
408 if (playlistId < 0) {
409 // insert playlist failed
410 return;
411 }
412 // insert item to FM recording play list
413 addToPlaylist(context, playlistId, recordingId);
414 // scan to update duration
415 MediaScannerConnection.scanFile(context, new String[] { mRecordFile.getPath() },
416 null, null);
417 }
418
419 /**
420 * Get the play list ID
421 * @param context Current passed in Context instance
422 * @return The play list ID
423 */
424 public static int getPlaylistId(final Context context) {
425 Cursor playlistCursor = context.getContentResolver().query(
426 MediaStore.Audio.Playlists.getContentUri("external"),
427 new String[] {
428 MediaStore.Audio.Playlists._ID
429 },
430 MediaStore.Audio.Playlists.DATA + "=?",
431 new String[] {
432 FmUtils.getPlaylistPath(context) + RECORDING_FILE_SOURCE
433 },
434 null);
435 int playlistId = -1;
436 if (null != playlistCursor) {
437 try {
438 if (playlistCursor.moveToFirst()) {
439 playlistId = playlistCursor.getInt(0);
440 }
441 } finally {
442 playlistCursor.close();
443 }
444 }
445 return playlistId;
446 }
447
448 private int createPlaylist(final Context context) {
449 final int size = 1;
450 ContentValues cv = new ContentValues(size);
451 cv.put(MediaStore.Audio.Playlists.NAME, RECORDING_FILE_SOURCE);
452 Uri newPlaylistUri = context.getContentResolver().insert(
453 MediaStore.Audio.Playlists.getContentUri("external"), cv);
454 if (newPlaylistUri == null) {
455 Log.d(TAG, "createPlaylist, create playlist failed");
456 return -1;
457 }
458 return Integer.valueOf(newPlaylistUri.getLastPathSegment());
459 }
460
461 private int addToAudioTable(final Context context, final ContentValues cv) {
462 ContentResolver resolver = context.getContentResolver();
463 int id = -1;
464
465 Cursor cursor = null;
466
467 try {
468 cursor = resolver.query(
469 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
470 new String[] { MediaStore.Audio.Media._ID },
471 MediaStore.Audio.Media.DATA + "=?",
472 new String[] { mRecordFile.getPath() },
473 null);
474 if (cursor != null && cursor.moveToFirst()) {
475 // Exist in database, just update it
476 id = cursor.getInt(0);
477 resolver.update(ContentUris.withAppendedId(
478 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id),
479 cv,
480 null,
481 null);
482 } else {
483 // insert new entry to database
484 Uri uri = context.getContentResolver().insert(
485 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, cv);
486 if (uri != null) {
487 id = Integer.valueOf(uri.getLastPathSegment());
488 }
489 }
490 } finally {
491 if (cursor != null) {
492 cursor.close();
493 }
494 }
495 return id;
496 }
497
498 private void addToPlaylist(final Context context, final int playlistId, final int recordingId) {
499 ContentResolver resolver = context.getContentResolver();
500 Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId);
501 int order = 0;
502 Cursor cursor = null;
503 try {
504 cursor = resolver.query(
505 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
506 new String[] { MediaStore.Audio.Media._ID },
507 MediaStore.Audio.Media.DATA + "=?",
508 new String[] { mRecordFile.getPath() },
509 null);
510 if (cursor != null && cursor.moveToFirst()) {
511 // Exist in database, just update it
512 order = cursor.getCount();
513 }
514 } finally {
515 if (cursor != null) {
516 cursor.close();
517 }
518 }
519 ContentValues cv = new ContentValues(2);
520 cv.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, recordingId);
521 cv.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, order);
522 context.getContentResolver().insert(uri, cv);
523 }
524
525 private void stopRecorder() {
526 synchronized (this) {
527 if (mRecorder != null) {
528 try {
529 mRecorder.stop();
530 } catch (IllegalStateException ex) {
531 Log.e(TAG, "stopRecorder, IllegalStateException ocurr " + ex);
532 setError(ERROR_RECORDER_INTERNAL);
533 } finally {
534 mRecorder.release();
535 mRecorder = null;
536 }
537 }
538 }
539 }
540}