/* * Copyright 2012 The Stanford MobiSocial Laboratory * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package mobisocial.musubi; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.Date; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import mobisocial.musubi.feed.presence.Push2TalkPresence; import mobisocial.musubi.model.helpers.DatabaseFile; import mobisocial.musubi.objects.VoiceObj; import mobisocial.musubi.service.MusubiService; import mobisocial.socialkit.musubi.Musubi; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.res.AssetFileDescriptor; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioRecord; import android.media.AudioTrack; import android.media.MediaPlayer; import android.media.MediaRecorder; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; /** * Record a voice note after long-pressing the 'volume up' key. * */ public class VoiceRecordActivity extends Activity implements RemoteControlReceiver.SpecialKeyEventHandler { private static final String TAG = "msb-voicerecording"; private static final String AUDIO_RECORDER_FOLDER = "DungBeetleTemp"; private static final String AUDIO_RECORDER_TEMP_FILE = "record_temp.raw"; private static final int RECORDER_SAMPLERATE = 8000; private static final int RECORDER_CHANNELS = AudioFormat.CHANNEL_CONFIGURATION_MONO; private static final int RECORDER_AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT; private Uri feedUri; private boolean mSoundFinished = false; private Set<Uri> presenceUris; private AudioRecord recorder = null; private AudioTrack track = null; private int bufferSize = 0; private Thread recordingThread = null; private boolean doneRecording = false; private byte rawBytes[] = null; private Timer mTimer; private Date mStart; private ProgressBar mTimerRecord; private TextView mStatusLabel; private Musubi mMusubi; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mMusubi = App.getMusubi(this); setContentView(R.layout.voice_quick_recorder); Intent intent = getIntent(); if (intent.hasExtra("feed_uri")) { feedUri = intent.getParcelableExtra("feed_uri"); } else if (intent.hasExtra("presence_mode")) { presenceUris = Push2TalkPresence.getInstance().getFeedsWithPresence(); if (presenceUris.size() == 0) { presenceUris = null; } findViewById(R.id.sendRecord).setVisibility(View.GONE); } if (presenceUris == null && feedUri == null) { Toast.makeText(this, "No recipients for voice recording.", Toast.LENGTH_SHORT).show(); finish(); return; } //block rotate as it kills the activity int orientation = getResources().getConfiguration().orientation; setRequestedOrientation(orientation); mTimerRecord = (ProgressBar)findViewById(R.id.timerRecord); mTimerRecord.setMax(18*2); mStatusLabel = (TextView)findViewById(R.id.statusLabel); ((Button)findViewById(R.id.cancelRecord)).setOnClickListener(btnClick); ((Button)findViewById(R.id.sendRecord)).setOnClickListener(btnClick); RemoteControlReceiver.setSpecialKeyEventHandler(this); bufferSize = AudioRecord.getMinBufferSize(RECORDER_SAMPLERATE,RECORDER_CHANNELS,RECORDER_AUDIO_ENCODING); notifyStartRecording(); //in case there was an FC, we must restart the service whenever one of our dialogs is opened. startService(new Intent(this, MusubiService.class)); } @Override public void onDestroy() { super.onDestroy(); RemoteControlReceiver.clearSpecialKeyEventHandler(); } private String getTempFilename(){ String filepath = Environment.getExternalStorageDirectory().getPath(); File file = new File(filepath,AUDIO_RECORDER_FOLDER); if(!file.exists()){ file.mkdirs(); } File tempFile = new File(filepath,AUDIO_RECORDER_TEMP_FILE); if(tempFile.exists()) tempFile.delete(); return (file.getAbsolutePath() + "/" + AUDIO_RECORDER_TEMP_FILE); } private void notifyStartRecording() { MediaPlayer player = getMediaPlayer(this, R.raw.videorecord); player.start(); player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { mSoundFinished = true; startRecording(); mp.release(); } }); } private void notifySendRecording() { MediaPlayer player = getMediaPlayer(this, R.raw.dontpanic); player.start(); player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { mp.release(); } }); } private void startRecording(){ mStatusLabel.setText("Recording..."); recorder = new AudioRecord(MediaRecorder.AudioSource.MIC, RECORDER_SAMPLERATE, RECORDER_CHANNELS,RECORDER_AUDIO_ENCODING, bufferSize); mStart = new Date(); mTimer = new Timer(); recorder.startRecording(); recordingThread = new Thread(new Runnable() { @Override public void run() { try { writeAudioDataToFile(); } finally { //for some cases if we stop this before the above loop, then it blocks for ever on write! recorder.stop(); recorder.release(); mTimer.cancel(); } } },"AudioRecorder Thread"); //must start after setup mTimer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { runOnUiThread(new Runnable() { @Override public void run() { Date now = new Date(); long millis = now.getTime() - mStart.getTime(); if(millis > 18000) { millis = 18000; stopRecording(); } mTimerRecord.setProgress((int)millis / (1000 / 2)); } }); } }, 1000, 500); recordingThread.start(); } private void writeAudioDataToFile(){ byte data[] = new byte[bufferSize]; String filename = getTempFilename(); FileOutputStream out = null; try { out = new FileOutputStream(filename); int read = 0; int total = 0; if(null != out){ //TODO:XXX Kill this whole damn class. Its full of race conditions in the logic while(!doneRecording && total < DatabaseFile.SIZE_LIMIT * 3 / 4 ){ read = recorder.read(data, 0, bufferSize); if(AudioRecord.ERROR_INVALID_OPERATION == read) break; try { out.write(data); } catch (IOException e) { e.printStackTrace(); break; } total += read; } } } catch (IOException e) { Log.e(TAG, "failed during voice record", e); } finally { try { if(out != null) out.close(); } catch (IOException e) { Log.e(TAG, "failed to close during voice record", e); } } } private void stopRecording() { if(doneRecording) return; doneRecording = true; mStatusLabel.setText("Stopped"); while(recordingThread == null) { try { Thread.sleep(3000); } catch (InterruptedException e1) { e1.printStackTrace(); } //must have been abandoned! if(!mSoundFinished) return; } synchronized(this) { try { recordingThread.join(); } catch (InterruptedException e) { e.printStackTrace(); } loadIntoBytes(getTempFilename()); deleteTempFile(); } } private void sendRecording() { if (rawBytes == null) { Log.e(TAG, "No audio bytes to send"); finish(); return; } notifySendRecording(); if (feedUri != null) { mMusubi.getFeed(feedUri).postObj(VoiceObj.from(rawBytes)); } else if (presenceUris != null) { Helpers.sendToFeeds(getApplicationContext(), VoiceObj.from(rawBytes), presenceUris); } else { Log.d(TAG, "no one to send voice to!"); } finish(); } private void deleteTempFile() { File file = new File(getTempFilename()); file.delete(); } private void loadIntoBytes(String inFilename) { FileInputStream in = null; long totalAudioLen = 0; try { in = new FileInputStream(inFilename); totalAudioLen = in.getChannel().size(); if(totalAudioLen > DatabaseFile.SIZE_LIMIT * 3 / 4) totalAudioLen = DatabaseFile.SIZE_LIMIT * 3 / 4; rawBytes = new byte[(int)totalAudioLen]; track = new AudioTrack(AudioManager.STREAM_MUSIC, RECORDER_SAMPLERATE, RECORDER_CHANNELS, RECORDER_AUDIO_ENCODING, (int)totalAudioLen, AudioTrack.MODE_STATIC); int offset = 0; int numRead = 0; while (offset < rawBytes.length && (numRead=in.read(rawBytes, offset, rawBytes.length-offset)) >= 0) { offset += numRead; } track.write(rawBytes, 0, (int)totalAudioLen); // TODO: Full resolution to Content Corral (if in some mode) //track.play(); } catch (FileNotFoundException e) { rawBytes = null; Log.e(TAG, "Error loading audio to bytes", e); } catch (IOException e) { rawBytes = null; Log.e(TAG, "Error loading audio to bytes", e); } catch (IllegalArgumentException e) { rawBytes = null; Log.e(TAG, "Error loading audio to bytes", e); } finally { try { if(in != null) in.close(); } catch (IOException e) { Log.e(TAG, "failed to close voice record inputs stream", e); } } } private static MediaPlayer getMediaPlayer(Context context, int resid) { try { AssetFileDescriptor afd = context.getResources().openRawResourceFd(resid); if (afd == null) return null; MediaPlayer mp = new MediaPlayer(); //mp.setAudioStreamType(AudioManager.STREAM_RING); mp.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); afd.close(); mp.prepare(); return mp; } catch (IOException ex) { Log.d(TAG, "create failed:", ex); // fall through } catch (IllegalArgumentException ex) { Log.d(TAG, "create failed:", ex); // fall through } catch (SecurityException ex) { Log.d(TAG, "create failed:", ex); // fall through } return null; } private View.OnClickListener btnClick = new View.OnClickListener() { @Override public void onClick(View v) { stopRecording(); if (v.getId() == R.id.cancelRecord) { stopRecording(); finish(); return; } if (rawBytes == null) { Toast toast = Toast.makeText(getApplicationContext(), "Please record a message first", Toast.LENGTH_SHORT); toast.show(); } else { sendRecording(); } } }; @Override public boolean onKeyDown(int keyCode, KeyEvent event) { mSpecialEvent = false; event.startTracking(); return true; }; @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK) { if ((mSpecialEvent || event.isTracking())) { stopRecording(); sendRecording(); } return true; } return false; } private boolean mSpecialEvent = false; @Override public boolean onSpecialKeyEvent(KeyEvent event) { int action = event.getAction(); mSpecialEvent = true; if (action == KeyEvent.ACTION_DOWN) { return onKeyDown(event.getKeyCode(), event); } else if (action == KeyEvent.ACTION_UP) { return onKeyUp(event.getKeyCode(), event); } return false; }; }