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