1 /*
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 
17 package com.android.fmradio;
18 
19 import android.content.ContentResolver;
20 import android.content.ContentUris;
21 import android.content.ContentValues;
22 import android.content.Context;
23 import android.database.Cursor;
24 import android.media.MediaPlayer;
25 import android.media.MediaRecorder;
26 import android.media.MediaScannerConnection;
27 import android.net.Uri;
28 import android.os.Environment;
29 import android.os.SystemClock;
30 import android.provider.MediaStore;
31 import android.text.format.DateFormat;
32 import android.util.Log;
33 
34 import java.io.File;
35 import java.io.IOException;
36 import java.text.SimpleDateFormat;
37 import java.util.Date;
38 import java.util.Locale;
39 
40 /**
41  * This class provider interface to recording, stop recording, save recording
42  * file, play recording file
43  */
44 public 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      */
startRecording(Context context)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);
152             mRecorder.setAudioSource(MediaRecorder.AudioSource.FM_TUNER);
153             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      */
stopRecording()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      */
getRecordTime()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      */
getState()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      */
getRecordFileName()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      */
saveRecording(Context context, String newName)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      */
discardRecording()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      */
registerRecorderStateListener(OnRecorderStateChangedListener listener)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          */
onRecorderStateChanged(int state)290         void onRecorderStateChanged(int state);
291 
292         /**
293          * notify FM recorder error message
294          *
295          * @param error error type
296          */
onRecorderError(int error)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
onError(MediaRecorder mr, int what, int extra)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
onInfo(MediaRecorder mr, int what, int extra)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      */
resetRecorder()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      */
setError(int error)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      */
setState(int state)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      */
addRecordingToDatabase(final Context context)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      */
getPlaylistId(final Context context)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 
createPlaylist(final Context context)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 
addToAudioTable(final Context context, final ContentValues cv)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 
addToPlaylist(final Context context, final int playlistId, final int recordingId)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 
stopRecorder()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 }
541