/* * Copyright (c) 2013, The Linux Foundation. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of The Linux Foundation nor * the names of its contributors may be used to endorse or promote * products derived from this software without specific prior written * permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.codeaurora.fmrecording; import java.util.*; import android.app.Service; import java.io.IOException; import java.lang.ref.WeakReference; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.BroadcastReceiver; import android.media.AudioManager; import android.media.AudioManager.OnAudioFocusChangeListener; import android.media.AudioSystem; import android.media.MediaRecorder; import android.os.Environment; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.util.Log; import java.util.Date; import java.text.SimpleDateFormat; import android.provider.MediaStore; import android.content.ContentResolver; import android.content.ContentValues; import android.database.Cursor; import java.io.File; import android.widget.Toast; import android.os.UserHandle; import android.net.Uri; import android.content.res.Resources; import android.os.StatFs; import android.app.Notification; import android.app.NotificationManager; import android.widget.RemoteViews; import android.R.layout; import android.R.drawable; import android.content.ComponentName; import android.content.res.Resources; import android.os.Process; import android.app.ActivityManager; import android.app.ActivityManager.RunningAppProcessInfo; public class FMRecordingService extends Service { private static final String TAG = "FMRecordingService"; private BroadcastReceiver mFmRecordingReceiver = null; private BroadcastReceiver mFmShutdownReceiver = null; public static final long UNAVAILABLE = -1L; public static final long PREPARING = -2L; public static final long UNKNOWN_SIZE = -3L; public static final long LOW_STORAGE_THRESHOLD = 50000000; private long mStorageSpace; private boolean mFmRecordingOn = false; public static final String ACTION_FM_RECORDING = "codeaurora.intent.action.FM_Recording"; public static final String ACTION_FM_RECORDING_STATUS = "codeaurora.intent.action.FM.Recording.Status"; private File mSampleFile = null; private MediaRecorder mRecorder = null; private long mSampleStart = 0; static final int START = 1; static final int STOP = 0; private int mRecordDuration = -1; private Thread mStatusCheckThread = null; private int clientPid = -1; private String clientProcessName = ""; private String mAudioType = "audio/*"; private BroadcastReceiver mSdcardUnmountReceiver = null; public void onCreate() { super.onCreate(); Log.d(TAG, "FMRecording Service onCreate"); registerRecordingListner(); registerShutdownListner(); registerStorageMediaListener(); } public int onStartCommand(Intent intent, int flags, int startId) { Log.d(TAG, "FMRecording Service onCreate"); return START_NOT_STICKY; } public void onDestroy() { Log.d(TAG, "FMRecording Service onDestroy"); if (mFmRecordingOn == true) { Log.d(TAG, "Still recording on progress, Stoping it"); stopRecord(); } unregisterBroadCastReceiver(mFmRecordingReceiver); unregisterBroadCastReceiver(mFmShutdownReceiver); unregisterBroadCastReceiver(mSdcardUnmountReceiver); super.onDestroy(); } public IBinder onBind(Intent intent) { Log.v(TAG, "FMRecording Service onBind"); return null; } private void unregisterBroadCastReceiver(BroadcastReceiver myreceiver) { if (myreceiver != null) { unregisterReceiver(myreceiver); myreceiver = null; } } private void registerShutdownListner() { if (mFmShutdownReceiver == null) { mFmShutdownReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Log.d(TAG, "Received intent " +intent); String action = intent.getAction(); Log.d(TAG, " action = " +action); if (action.equals("android.intent.action.ACTION_SHUTDOWN")) { Log.d(TAG, "android.intent.action.ACTION_SHUTDOWN Intent received"); stopRecord(); } } }; IntentFilter iFilter = new IntentFilter(); iFilter.addAction("android.intent.action.ACTION_SHUTDOWN"); registerReceiver(mFmShutdownReceiver, iFilter); } } private static long getAvailableSpace() { String state = Environment.getExternalStorageState(); Log.d(TAG, "External storage state=" + state); if (Environment.MEDIA_CHECKING.equals(state)) { return PREPARING; } if (!Environment.MEDIA_MOUNTED.equals(state)) { return UNAVAILABLE; } try { File sampleDir = Environment.getExternalStorageDirectory(); StatFs stat = new StatFs(sampleDir.getAbsolutePath()); return stat.getAvailableBlocks() * (long) stat.getBlockSize(); } catch (Exception e) { Log.i(TAG, "Fail to access external storage", e); } return UNKNOWN_SIZE; } private boolean updateAndShowStorageHint() { mStorageSpace = getAvailableSpace(); return showStorageHint(); } private boolean showStorageHint() { String errorMessage = null; if (mStorageSpace == UNAVAILABLE) { errorMessage = getString(R.string.no_storage); } else if (mStorageSpace == PREPARING) { errorMessage = getString(R.string.preparing_sd); } else if (mStorageSpace == UNKNOWN_SIZE) { errorMessage = getString(R.string.access_sd_fail); } else if (mStorageSpace < LOW_STORAGE_THRESHOLD) { errorMessage = getString(R.string.spaceIsLow_content); } if (errorMessage != null) { Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show(); return false; } return true; } private void registerStorageMediaListener() { if (mSdcardUnmountReceiver == null) { mSdcardUnmountReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if ((action.equals(Intent.ACTION_MEDIA_UNMOUNTED)) || action.equals(Intent.ACTION_MEDIA_EJECT)) { Log.d(TAG, "ACTION_MEDIA_UNMOUNTED Intent received"); if (mFmRecordingOn == true) { try { stopRecord(); } catch (Exception e) { e.printStackTrace(); } } } } }; IntentFilter iFilter = new IntentFilter(); iFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); iFilter.addAction(Intent.ACTION_MEDIA_EJECT); iFilter.addDataScheme("file"); registerReceiver(mSdcardUnmountReceiver, iFilter); } } private void sendRecordingStatusIntent(int status) { Intent intent = new Intent(ACTION_FM_RECORDING_STATUS); intent.putExtra("state", status); Log.d(TAG, "posting intent for FM Recording status as = " +status); getApplicationContext().sendBroadcastAsUser(intent, UserHandle.ALL); } private boolean startRecord() { Log.d(TAG, "Enter startRecord"); if (mRecorder != null) { /* Stop existing recording if any */ Log.d(TAG, "Stopping existing record"); try { mRecorder.stop(); mRecorder.reset(); mRecorder.release(); mRecorder = null; } catch(Exception e) { e.printStackTrace(); } } if (!updateAndShowStorageHint()) return false; long maxFileSize = mStorageSpace - LOW_STORAGE_THRESHOLD; mRecorder = new MediaRecorder(); try { mRecorder.setMaxFileSize(maxFileSize); if(mRecordDuration >= 0) mRecorder.setMaxDuration(mRecordDuration); } catch (RuntimeException exception) { } mSampleFile = null; File sampleDir = getExternalCacheDir(); try { mSampleFile = new File(sampleDir, "FMRecording.3gpp"); mSampleFile.createNewFile(); } catch (IOException e) { Log.e(TAG, "Not able to access SD Card"); Toast.makeText(this, "Not able to access SD Card", Toast.LENGTH_SHORT).show(); } try { Log.d(TAG, "AudioSource.FM_RX" +MediaRecorder.AudioSource.FM_RX); mRecorder.setAudioSource(MediaRecorder.AudioSource.FM_RX); mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); mAudioType = "audio/3gpp"; } catch (RuntimeException exception) { Log.d(TAG, "RuntimeException while settings"); mRecorder.reset(); mRecorder.release(); mRecorder = null; return false; } Log.d(TAG, "setOutputFile"); mRecorder.setOutputFile(mSampleFile.getAbsolutePath()); try { mRecorder.prepare(); Log.d(TAG, "start"); mRecorder.start(); } catch (IOException e) { Log.d(TAG, "IOException while start"); mRecorder.reset(); mRecorder.release(); mRecorder = null; return false; } catch (RuntimeException e) { Log.d(TAG, "RuntimeException while start"); mRecorder.reset(); mRecorder.release(); mRecorder = null; return false; } mFmRecordingOn = true; Log.d(TAG, "mSampleFile.getAbsolutePath() " +mSampleFile.getAbsolutePath()); mRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() { public void onInfo(MediaRecorder mr, int what, int extra) { if ((what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) || (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED)) { if (mFmRecordingOn) { Log.d(TAG, "Maximum file size/duration reached, stopping the recording"); stopRecord(); } // Show the toast. Toast.makeText(FMRecordingService.this, R.string.FMRecording_reach_size_limit, Toast.LENGTH_LONG).show(); } } // from MediaRecorder.OnErrorListener public void onError(MediaRecorder mr, int what, int extra) { Log.e(TAG, "MediaRecorder error. what=" + what + ". extra=" + extra); if (what == MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN) { // We may have run out of space on the sdcard. if (mFmRecordingOn) { stopRecord(); } updateAndShowStorageHint(); } } }); mSampleStart = System.currentTimeMillis(); sendRecordingStatusIntent(START); startNotification(); return true; } private void startNotification() { RemoteViews views = new RemoteViews(getPackageName(), R.layout.record_status_bar); Notification status = new Notification(); status.contentView = views; status.flags |= Notification.FLAG_ONGOING_EVENT; status.icon = R.drawable.ic_menu_record; startForeground(102, status); } private void stopRecord() { Log.d(TAG, "Enter stopRecord"); mFmRecordingOn = false; if (mRecorder == null) return; try { mRecorder.stop(); mRecorder.reset(); mRecorder.release(); mRecorder = null; } catch(Exception e) { e.printStackTrace(); } sendRecordingStatusIntent(STOP); saveFile(); stopForeground(true); stopClientStatusCheck(); } private void saveFile() { int sampleLength = (int)((System.currentTimeMillis() - mSampleStart)/1000 ); Log.d(TAG, "Enter saveFile"); if (sampleLength == 0) return; String state = Environment.getExternalStorageState(); Log.d(TAG, "storage state is " + state); if (Environment.MEDIA_MOUNTED.equals(state)) { File finalFile; try { finalFile = File.createTempFile("FMRecording", ".3gpp", Environment.getExternalStorageDirectory()); mSampleFile.renameTo(finalFile); mSampleFile = finalFile; } catch (IOException e) { Log.e(TAG, "Not able to access SD Card"); Toast.makeText(this, "Not able to access SD Card", Toast.LENGTH_SHORT).show(); e.printStackTrace(); } try { this.addToMediaDB(mSampleFile); Toast.makeText(this,getString(R.string.save_record_file, mSampleFile.getAbsolutePath( )), Toast.LENGTH_LONG).show(); } catch(Exception e) { e.printStackTrace(); } } else { Log.e(TAG, "SD card must have removed during recording. "); Toast.makeText(this, "Recording aborted", Toast.LENGTH_SHORT).show(); } return; } /* * Adds file and returns content uri. */ private Uri addToMediaDB(File file) { Log.d(TAG, "In addToMediaDB"); Resources res = getResources(); ContentValues cv = new ContentValues(); long current = System.currentTimeMillis(); long modDate = file.lastModified(); Date date = new Date(current); SimpleDateFormat formatter = new SimpleDateFormat( res.getString(R.string.audio_db_title_format)); String title = formatter.format(date); // Lets label the recorded audio file as NON-MUSIC so that the file // won't be displayed automatically, except for in the playlist. cv.put(MediaStore.Audio.Media.IS_MUSIC, "1"); cv.put(MediaStore.Audio.Media.TITLE, title); cv.put(MediaStore.Audio.Media.DATA, file.getAbsolutePath()); cv.put(MediaStore.Audio.Media.DATE_ADDED, (int) (current / 1000)); cv.put(MediaStore.Audio.Media.DATE_MODIFIED, (int) (modDate / 1000)); cv.put(MediaStore.Audio.Media.MIME_TYPE, mAudioType); cv.put(MediaStore.Audio.Media.ARTIST, res.getString(R.string.audio_db_artist_name)); cv.put(MediaStore.Audio.Media.ALBUM, res.getString(R.string.audio_db_album_name)); Log.d(TAG, "Inserting audio record: " + cv.toString()); ContentResolver resolver = getContentResolver(); Uri base = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; Log.d(TAG, "ContentURI: " + base); Uri result = resolver.insert(base, cv); if (result == null) { Toast.makeText(this, "Unable to save recorded audio", Toast.LENGTH_SHORT).show(); return null; } if (getPlaylistId(res) == -1) { createPlaylist(res, resolver); } int audioId = Integer.valueOf(result.getLastPathSegment()); addToPlaylist(resolver, audioId, getPlaylistId(res)); // Notify those applications such as Music listening to the // scanner events that a recorded audio file just created. sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, result)); return result; } private int getPlaylistId(Resources res) { Uri uri = MediaStore.Audio.Playlists.getContentUri("external"); final String[] ids = new String[] { MediaStore.Audio.Playlists._ID }; final String where = MediaStore.Audio.Playlists.NAME + "=?"; final String[] args = new String[] { res.getString(R.string.audio_db_playlist_name) }; Cursor cursor = query(uri, ids, where, args, null); if (cursor == null) { Log.v(TAG, "query returns null"); } int id = -1; if (cursor != null) { cursor.moveToFirst(); if (!cursor.isAfterLast()) { id = cursor.getInt(0); } cursor.close(); } return id; } private Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { try { ContentResolver resolver = getContentResolver(); if (resolver == null) { return null; } return resolver.query(uri, projection, selection, selectionArgs, sortOrder); } catch (UnsupportedOperationException ex) { return null; } } private Uri createPlaylist(Resources res, ContentResolver resolver) { ContentValues cv = new ContentValues(); cv.put(MediaStore.Audio.Playlists.NAME, res.getString(R.string.audio_db_playlist_name)); Uri uri = resolver.insert(MediaStore.Audio.Playlists.getContentUri("external"), cv); if (uri == null) { Toast.makeText(this, "Unable to save recorded audio", Toast.LENGTH_SHORT).show(); } return uri; } private void addToPlaylist(ContentResolver resolver, int audioId, long playlistId) { String[] cols = new String[] { "count(*)" }; Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId); Cursor cur = resolver.query(uri, cols, null, null, null); final int base; if (cur != null) { cur.moveToFirst(); base = cur.getInt(0); cur.close(); } else { base = 0; } ContentValues values = new ContentValues(); values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(base + audioId)); values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audioId); resolver.insert(uri, values); } private void registerRecordingListner() { if (mFmRecordingReceiver == null) { mFmRecordingReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Log.d(TAG, "Received intent " +intent); String action = intent.getAction(); Log.d(TAG, " action = " +action); if (action.equals(ACTION_FM_RECORDING)) { int state = intent.getIntExtra("state", STOP); Log.d(TAG, "ACTION_FM_RECORDING Intent received" + state); if (state == START) { Log.d(TAG, "Recording start"); mRecordDuration = intent.getIntExtra("record_duration", mRecordDuration); if(startRecord()) { clientProcessName = intent.getStringExtra("process_name"); clientPid = intent.getIntExtra("process_id", -1); startClientStatusCheck(); } } else if (state == STOP) { Log.d(TAG, "Stop recording"); stopRecord(); } } } }; IntentFilter iFilter = new IntentFilter(); iFilter.addAction(ACTION_FM_RECORDING); registerReceiver(mFmRecordingReceiver, iFilter); } } private boolean getClientStatus(int pid, String processName) { boolean status = false; ActivityManager actvityManager = (ActivityManager)this.getSystemService( this.ACTIVITY_SERVICE); List procInfos = actvityManager.getRunningAppProcesses(); for(RunningAppProcessInfo procInfo : procInfos) { if ((pid == procInfo.pid) && (procInfo.processName.equals(processName))) { status = true; break; } } procInfos.clear(); return status; } private Runnable clientStatusCheckThread = new Runnable() { @Override public void run() { while(!Thread.currentThread().isInterrupted()) { try { if(!getClientStatus(clientPid, clientProcessName)) { stopRecord(); break; }; Thread.sleep(500); }catch(Exception e) { Log.d(TAG, "Client status check thread interrupted"); break; } } } }; private void startClientStatusCheck() { if((mStatusCheckThread == null) || (mStatusCheckThread.getState() == Thread.State.TERMINATED)) { mStatusCheckThread = new Thread(null, clientStatusCheckThread, "GetClientStatus"); } if((mStatusCheckThread != null) && (mStatusCheckThread.getState() == Thread.State.NEW)) { mStatusCheckThread.start(); } } private void stopClientStatusCheck() { if(mStatusCheckThread != null) { mStatusCheckThread.interrupt(); } } }