@@ -21,6 +21,8 @@ import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
@@ -30,167 +32,180 @@ import eu.siacs.conversations.utils.ThemeHelper;
public class RecordingActivity extends Activity implements View.OnClickListener {
- public static String STORAGE_DIRECTORY_TYPE_NAME = "Recordings";
-
- private ActivityRecordingBinding binding;
-
- private MediaRecorder mRecorder;
- private long mStartTime = 0;
-
- private Handler mHandler = new Handler();
- private Runnable mTickExecutor = new Runnable() {
- @Override
- public void run() {
- tick();
- mHandler.postDelayed(mTickExecutor, 100);
- }
- };
-
- private File mOutputFile;
- private boolean mShouldFinishAfterWrite = false;
-
- private FileObserver mFileObserver;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- setTheme(ThemeHelper.findDialog(this));
- super.onCreate(savedInstanceState);
- this.binding = DataBindingUtil.setContentView(this,R.layout.activity_recording);
- this.binding.cancelButton.setOnClickListener(this);
- this.binding.shareButton.setOnClickListener(this);
- this.setFinishOnTouchOutside(false);
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- if (!startRecording()) {
- this.binding.shareButton.setEnabled(false);
- this.binding.timer.setTextAppearance(this, R.style.TextAppearance_Conversations_Title);
- this.binding.timer.setText(R.string.unable_to_start_recording);
- }
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- if (mRecorder != null) {
- mHandler.removeCallbacks(mTickExecutor);
- stopRecording(false);
- }
- if (mFileObserver != null) {
- mFileObserver.stopWatching();
- }
- }
-
- private boolean startRecording() {
- mRecorder = new MediaRecorder();
- mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
- mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
- mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
- mRecorder.setAudioEncodingBitRate(96000);
- mRecorder.setAudioSamplingRate(22050);
- setupOutputFile();
- mRecorder.setOutputFile(mOutputFile.getAbsolutePath());
-
- try {
- mRecorder.prepare();
- mRecorder.start();
- mStartTime = SystemClock.elapsedRealtime();
- mHandler.postDelayed(mTickExecutor, 100);
- Log.d("Voice Recorder", "started recording to " + mOutputFile.getAbsolutePath());
- return true;
- } catch (Exception e) {
- Log.e("Voice Recorder", "prepare() failed " + e.getMessage());
- return false;
- }
- }
-
- protected void stopRecording(boolean saveFile) {
- mShouldFinishAfterWrite = saveFile;
- try {
- mRecorder.stop();
- mRecorder.release();
- } catch (Exception e) {
- if (saveFile) {
- Toast.makeText(this,R.string.unable_to_save_recording, Toast.LENGTH_SHORT).show();
- }
- } finally {
- mRecorder = null;
- mStartTime = 0;
- }
- if (!saveFile && mOutputFile != null) {
- if (mOutputFile.delete()) {
- Log.d(Config.LOGTAG,"deleted canceled recording");
- }
- }
- }
-
- private static File generateOutputFilename(Context context) {
- SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US);
- String filename = "RECORDING_" + dateFormat.format(new Date()) + ".m4a";
- return new File(FileBackend.getConversationsDirectory(context, STORAGE_DIRECTORY_TYPE_NAME) + "/" + filename);
- }
-
- private void setupOutputFile() {
- mOutputFile = generateOutputFilename(this);
- File parentDirectory = mOutputFile.getParentFile();
- if (parentDirectory.mkdirs()) {
- Log.d(Config.LOGTAG, "created " + parentDirectory.getAbsolutePath());
- }
- File noMedia = new File(parentDirectory, ".nomedia");
- if (!noMedia.exists()) {
- try {
- if (noMedia.createNewFile()) {
- Log.d(Config.LOGTAG, "created nomedia file in " + parentDirectory.getAbsolutePath());
- }
- } catch (IOException e) {
- Log.d(Config.LOGTAG, "unable to create nomedia file in " + parentDirectory.getAbsolutePath(), e);
- }
- }
- setupFileObserver(parentDirectory);
- }
-
- private void setupFileObserver(File directory) {
- mFileObserver = new FileObserver(directory.getAbsolutePath()) {
- @Override
- public void onEvent(int event, String s) {
- if (s != null && s.equals(mOutputFile.getName()) && event == FileObserver.CLOSE_WRITE) {
- if (mShouldFinishAfterWrite) {
- setResult(Activity.RESULT_OK, new Intent().setData(Uri.fromFile(mOutputFile)));
- finish();
- }
- }
- }
- };
- mFileObserver.startWatching();
- }
-
- @SuppressLint("SetTextI18n")
- private void tick() {
- long time = (mStartTime < 0) ? 0 : (SystemClock.elapsedRealtime() - mStartTime);
- int minutes = (int) (time / 60000);
- int seconds = (int) (time / 1000) % 60;
- int milliseconds = (int) (time / 100) % 10;
- this.binding.timer.setText(minutes + ":" + (seconds < 10 ? "0" + seconds : seconds) + "." + milliseconds);
- }
-
- @Override
- public void onClick(View view) {
- switch (view.getId()) {
- case R.id.cancel_button:
- mHandler.removeCallbacks(mTickExecutor);
- stopRecording(false);
- setResult(RESULT_CANCELED);
- finish();
- break;
- case R.id.share_button:
- this.binding.shareButton.setEnabled(false);
- this.binding.shareButton.setText(R.string.please_wait);
- mHandler.removeCallbacks(mTickExecutor);
- mHandler.postDelayed(() -> stopRecording(true), 500);
- break;
- }
- }
+ public static String STORAGE_DIRECTORY_TYPE_NAME = "Recordings";
+
+ private ActivityRecordingBinding binding;
+
+ private MediaRecorder mRecorder;
+ private long mStartTime = 0;
+
+ private CountDownLatch outputFileWrittenLatch = new CountDownLatch(1);
+
+ private Handler mHandler = new Handler();
+ private Runnable mTickExecutor = new Runnable() {
+ @Override
+ public void run() {
+ tick();
+ mHandler.postDelayed(mTickExecutor, 100);
+ }
+ };
+
+ private File mOutputFile;
+
+ private FileObserver mFileObserver;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ setTheme(ThemeHelper.findDialog(this));
+ super.onCreate(savedInstanceState);
+ this.binding = DataBindingUtil.setContentView(this, R.layout.activity_recording);
+ this.binding.cancelButton.setOnClickListener(this);
+ this.binding.shareButton.setOnClickListener(this);
+ this.setFinishOnTouchOutside(false);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ if (!startRecording()) {
+ this.binding.shareButton.setEnabled(false);
+ this.binding.timer.setTextAppearance(this, R.style.TextAppearance_Conversations_Title);
+ this.binding.timer.setText(R.string.unable_to_start_recording);
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (mRecorder != null) {
+ mHandler.removeCallbacks(mTickExecutor);
+ stopRecording(false);
+ }
+ if (mFileObserver != null) {
+ mFileObserver.stopWatching();
+ }
+ }
+
+ private boolean startRecording() {
+ mRecorder = new MediaRecorder();
+ mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
+ mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
+ mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
+ mRecorder.setAudioEncodingBitRate(96000);
+ mRecorder.setAudioSamplingRate(22050);
+ setupOutputFile();
+ mRecorder.setOutputFile(mOutputFile.getAbsolutePath());
+
+ try {
+ mRecorder.prepare();
+ mRecorder.start();
+ mStartTime = SystemClock.elapsedRealtime();
+ mHandler.postDelayed(mTickExecutor, 100);
+ Log.d("Voice Recorder", "started recording to " + mOutputFile.getAbsolutePath());
+ return true;
+ } catch (Exception e) {
+ Log.e("Voice Recorder", "prepare() failed " + e.getMessage());
+ return false;
+ }
+ }
+
+ protected void stopRecording(final boolean saveFile) {
+ try {
+ mRecorder.stop();
+ mRecorder.release();
+ } catch (Exception e) {
+ if (saveFile) {
+ Toast.makeText(this, R.string.unable_to_save_recording, Toast.LENGTH_SHORT).show();
+ return;
+ }
+ } finally {
+ mRecorder = null;
+ mStartTime = 0;
+ }
+ if (!saveFile && mOutputFile != null) {
+ if (mOutputFile.delete()) {
+ Log.d(Config.LOGTAG, "deleted canceled recording");
+ }
+ }
+ if (saveFile) {
+ new Thread(() -> {
+ try {
+ if (!outputFileWrittenLatch.await(2, TimeUnit.SECONDS)) {
+ Log.d(Config.LOGTAG, "time out waiting for output file to be written");
+ }
+ } catch (InterruptedException e) {
+ Log.d(Config.LOGTAG, "interrupted while waiting for output file to be written" ,e);
+ }
+ runOnUiThread(() -> {
+ setResult(Activity.RESULT_OK, new Intent().setData(Uri.fromFile(mOutputFile)));
+ finish();
+ });
+ }).start();
+ }
+ }
+
+ private static File generateOutputFilename(Context context) {
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US);
+ String filename = "RECORDING_" + dateFormat.format(new Date()) + ".m4a";
+ return new File(FileBackend.getConversationsDirectory(context, STORAGE_DIRECTORY_TYPE_NAME) + "/" + filename);
+ }
+
+ private void setupOutputFile() {
+ mOutputFile = generateOutputFilename(this);
+ File parentDirectory = mOutputFile.getParentFile();
+ if (parentDirectory.mkdirs()) {
+ Log.d(Config.LOGTAG, "created " + parentDirectory.getAbsolutePath());
+ }
+ File noMedia = new File(parentDirectory, ".nomedia");
+ if (!noMedia.exists()) {
+ try {
+ if (noMedia.createNewFile()) {
+ Log.d(Config.LOGTAG, "created nomedia file in " + parentDirectory.getAbsolutePath());
+ }
+ } catch (IOException e) {
+ Log.d(Config.LOGTAG, "unable to create nomedia file in " + parentDirectory.getAbsolutePath(), e);
+ }
+ }
+ setupFileObserver(parentDirectory);
+ }
+
+ private void setupFileObserver(File directory) {
+ mFileObserver = new FileObserver(directory.getAbsolutePath()) {
+ @Override
+ public void onEvent(int event, String s) {
+ if (s != null && s.equals(mOutputFile.getName()) && event == FileObserver.CLOSE_WRITE) {
+ outputFileWrittenLatch.countDown();
+ }
+ }
+ };
+ mFileObserver.startWatching();
+ }
+
+ @SuppressLint("SetTextI18n")
+ private void tick() {
+ long time = (mStartTime < 0) ? 0 : (SystemClock.elapsedRealtime() - mStartTime);
+ int minutes = (int) (time / 60000);
+ int seconds = (int) (time / 1000) % 60;
+ int milliseconds = (int) (time / 100) % 10;
+ this.binding.timer.setText(minutes + ":" + (seconds < 10 ? "0" + seconds : seconds) + "." + milliseconds);
+ }
+
+ @Override
+ public void onClick(View view) {
+ switch (view.getId()) {
+ case R.id.cancel_button:
+ mHandler.removeCallbacks(mTickExecutor);
+ stopRecording(false);
+ setResult(RESULT_CANCELED);
+ finish();
+ break;
+ case R.id.share_button:
+ this.binding.shareButton.setEnabled(false);
+ this.binding.shareButton.setText(R.string.please_wait);
+ mHandler.removeCallbacks(mTickExecutor);
+ mHandler.postDelayed(() -> stopRecording(true), 500);
+ break;
+ }
+ }
}