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