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