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