1package eu.siacs.conversations.ui;
2
3import android.app.Activity;
4import android.content.Context;
5import android.content.Intent;
6import androidx.databinding.DataBindingUtil;
7import android.media.MediaRecorder;
8import android.net.Uri;
9import android.os.Bundle;
10import android.os.FileObserver;
11import android.os.Handler;
12import android.os.SystemClock;
13import android.util.Log;
14import android.view.View;
15import android.view.WindowManager;
16import android.widget.Toast;
17
18import java.io.File;
19import java.io.IOException;
20import java.text.SimpleDateFormat;
21import java.util.Date;
22import java.util.Locale;
23import java.util.concurrent.CountDownLatch;
24import java.util.concurrent.TimeUnit;
25
26import eu.siacs.conversations.Config;
27import eu.siacs.conversations.R;
28import eu.siacs.conversations.databinding.ActivityRecordingBinding;
29import eu.siacs.conversations.persistance.FileBackend;
30import eu.siacs.conversations.utils.ThemeHelper;
31import eu.siacs.conversations.utils.TimeFrameUtils;
32
33public class RecordingActivity extends Activity implements View.OnClickListener {
34
35 public static String STORAGE_DIRECTORY_TYPE_NAME = "Recordings";
36
37 private ActivityRecordingBinding binding;
38
39 private MediaRecorder mRecorder;
40 private long mStartTime = 0;
41
42 private CountDownLatch outputFileWrittenLatch = new CountDownLatch(1);
43
44 private Handler mHandler = new Handler();
45 private Runnable mTickExecutor = new Runnable() {
46 @Override
47 public void run() {
48 tick();
49 mHandler.postDelayed(mTickExecutor, 100);
50 }
51 };
52
53 private File mOutputFile;
54
55 private FileObserver mFileObserver;
56
57 @Override
58 protected void onCreate(Bundle savedInstanceState) {
59 setTheme(ThemeHelper.findDialog(this));
60 super.onCreate(savedInstanceState);
61 this.binding = DataBindingUtil.setContentView(this, R.layout.activity_recording);
62 this.binding.cancelButton.setOnClickListener(this);
63 this.binding.shareButton.setOnClickListener(this);
64 this.setFinishOnTouchOutside(false);
65 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
66 }
67
68 @Override
69 protected void onStart() {
70 super.onStart();
71 if (!startRecording()) {
72 this.binding.shareButton.setEnabled(false);
73 this.binding.timer.setTextAppearance(this, R.style.TextAppearance_Conversations_Title);
74 this.binding.timer.setText(R.string.unable_to_start_recording);
75 }
76 }
77
78 @Override
79 protected void onStop() {
80 super.onStop();
81 if (mRecorder != null) {
82 mHandler.removeCallbacks(mTickExecutor);
83 stopRecording(false);
84 }
85 if (mFileObserver != null) {
86 mFileObserver.stopWatching();
87 }
88 }
89
90 private boolean startRecording() {
91 mRecorder = new MediaRecorder();
92 mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
93 mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
94 mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
95 mRecorder.setAudioEncodingBitRate(96000);
96 mRecorder.setAudioSamplingRate(22050);
97 setupOutputFile();
98 mRecorder.setOutputFile(mOutputFile.getAbsolutePath());
99
100 try {
101 mRecorder.prepare();
102 mRecorder.start();
103 mStartTime = SystemClock.elapsedRealtime();
104 mHandler.postDelayed(mTickExecutor, 100);
105 Log.d("Voice Recorder", "started recording to " + mOutputFile.getAbsolutePath());
106 return true;
107 } catch (Exception e) {
108 Log.e("Voice Recorder", "prepare() failed " + e.getMessage());
109 return false;
110 }
111 }
112
113 protected void stopRecording(final boolean saveFile) {
114 try {
115 mRecorder.stop();
116 mRecorder.release();
117 } catch (Exception e) {
118 if (saveFile) {
119 Toast.makeText(this, R.string.unable_to_save_recording, Toast.LENGTH_SHORT).show();
120 return;
121 }
122 } finally {
123 mRecorder = null;
124 mStartTime = 0;
125 }
126 if (!saveFile && mOutputFile != null) {
127 if (mOutputFile.delete()) {
128 Log.d(Config.LOGTAG, "deleted canceled recording");
129 }
130 }
131 if (saveFile) {
132 new Thread(() -> {
133 try {
134 if (!outputFileWrittenLatch.await(2, TimeUnit.SECONDS)) {
135 Log.d(Config.LOGTAG, "time out waiting for output file to be written");
136 }
137 } catch (InterruptedException e) {
138 Log.d(Config.LOGTAG, "interrupted while waiting for output file to be written", e);
139 }
140 runOnUiThread(() -> {
141 setResult(Activity.RESULT_OK, new Intent().setData(Uri.fromFile(mOutputFile)));
142 finish();
143 });
144 }).start();
145 }
146 }
147
148 private static File generateOutputFilename(Context context) {
149 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US);
150 String filename = "RECORDING_" + dateFormat.format(new Date()) + ".m4a";
151 return new File(FileBackend.getConversationsDirectory(context, STORAGE_DIRECTORY_TYPE_NAME) + "/" + filename);
152 }
153
154 private void setupOutputFile() {
155 mOutputFile = generateOutputFilename(this);
156 File parentDirectory = mOutputFile.getParentFile();
157 if (parentDirectory.mkdirs()) {
158 Log.d(Config.LOGTAG, "created " + parentDirectory.getAbsolutePath());
159 }
160 File noMedia = new File(parentDirectory, ".nomedia");
161 if (!noMedia.exists()) {
162 try {
163 if (noMedia.createNewFile()) {
164 Log.d(Config.LOGTAG, "created nomedia file in " + parentDirectory.getAbsolutePath());
165 }
166 } catch (IOException e) {
167 Log.d(Config.LOGTAG, "unable to create nomedia file in " + parentDirectory.getAbsolutePath(), e);
168 }
169 }
170 setupFileObserver(parentDirectory);
171 }
172
173 private void setupFileObserver(File directory) {
174 mFileObserver = new FileObserver(directory.getAbsolutePath()) {
175 @Override
176 public void onEvent(int event, String s) {
177 if (s != null && s.equals(mOutputFile.getName()) && event == FileObserver.CLOSE_WRITE) {
178 outputFileWrittenLatch.countDown();
179 }
180 }
181 };
182 mFileObserver.startWatching();
183 }
184
185 private void tick() {
186 this.binding.timer.setText(TimeFrameUtils.formatTimePassed(mStartTime, true));
187 }
188
189 @Override
190 public void onClick(View view) {
191 switch (view.getId()) {
192 case R.id.cancel_button:
193 mHandler.removeCallbacks(mTickExecutor);
194 stopRecording(false);
195 setResult(RESULT_CANCELED);
196 finish();
197 break;
198 case R.id.share_button:
199 this.binding.shareButton.setEnabled(false);
200 this.binding.shareButton.setText(R.string.please_wait);
201 mHandler.removeCallbacks(mTickExecutor);
202 mHandler.postDelayed(() -> stopRecording(true), 500);
203 break;
204 }
205 }
206}