Home | History | Annotate | Download | only in fmradio
      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      */
     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.RADIO_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      */
    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 }
    541