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