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.preference.PreferenceManager;
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 static File generateOutputFilename(Context context) {
158 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US);
159 String filename = "RECORDING_" + dateFormat.format(new Date()) + ".m4a";
160 return new File(FileBackend.getConversationsDirectory(context, STORAGE_DIRECTORY_TYPE_NAME) + "/" + filename);
161 }
162
163 private void setupOutputFile() {
164 mOutputFile = generateOutputFilename(this);
165 File parentDirectory = mOutputFile.getParentFile();
166 if (parentDirectory.mkdirs()) {
167 Log.d(Config.LOGTAG, "created " + parentDirectory.getAbsolutePath());
168 }
169 File noMedia = new File(parentDirectory, ".nomedia");
170 if (!noMedia.exists()) {
171 try {
172 if (noMedia.createNewFile()) {
173 Log.d(Config.LOGTAG, "created nomedia file in " + parentDirectory.getAbsolutePath());
174 }
175 } catch (IOException e) {
176 Log.d(Config.LOGTAG, "unable to create nomedia file in " + parentDirectory.getAbsolutePath(), e);
177 }
178 }
179 setupFileObserver(parentDirectory);
180 }
181
182 private void setupFileObserver(File directory) {
183 mFileObserver = new FileObserver(directory.getAbsolutePath()) {
184 @Override
185 public void onEvent(int event, String s) {
186 if (s != null && s.equals(mOutputFile.getName()) && event == FileObserver.CLOSE_WRITE) {
187 outputFileWrittenLatch.countDown();
188 }
189 }
190 };
191 mFileObserver.startWatching();
192 }
193
194 private void tick() {
195 this.binding.timer.setText(TimeFrameUtils.formatTimePassed(mStartTime, true));
196 }
197
198 @Override
199 public void onClick(View view) {
200 switch (view.getId()) {
201 case R.id.cancel_button:
202 mHandler.removeCallbacks(mTickExecutor);
203 stopRecording(false);
204 setResult(RESULT_CANCELED);
205 finish();
206 break;
207 case R.id.share_button:
208 this.binding.shareButton.setEnabled(false);
209 this.binding.shareButton.setText(R.string.please_wait);
210 mHandler.removeCallbacks(mTickExecutor);
211 mHandler.postDelayed(() -> stopRecording(true), 500);
212 break;
213 }
214 }
215}