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.Button;
17import android.widget.TextView;
18import android.widget.Toast;
19
20import java.io.File;
21import java.io.IOException;
22import java.text.SimpleDateFormat;
23import java.util.Date;
24import java.util.Locale;
25
26import eu.siacs.conversations.Config;
27import eu.siacs.conversations.R;
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 TextView mTimerTextView;
36 private Button mCancelButton;
37 private Button mStopButton;
38
39 private MediaRecorder mRecorder;
40 private long mStartTime = 0;
41
42 private Handler mHandler = new Handler();
43 private Runnable mTickExecutor = new Runnable() {
44 @Override
45 public void run() {
46 tick();
47 mHandler.postDelayed(mTickExecutor, 100);
48 }
49 };
50
51 private File mOutputFile;
52 private boolean mShouldFinishAfterWrite = false;
53
54 private FileObserver mFileObserver;
55
56 @Override
57 protected void onCreate(Bundle savedInstanceState) {
58 setTheme(ThemeHelper.findDialog(this));
59 super.onCreate(savedInstanceState);
60 setContentView(R.layout.activity_recording);
61 this.mTimerTextView = (TextView) this.findViewById(R.id.timer);
62 this.mCancelButton = (Button) this.findViewById(R.id.cancel_button);
63 this.mCancelButton.setOnClickListener(this);
64 this.mStopButton = (Button) this.findViewById(R.id.share_button);
65 this.mStopButton.setOnClickListener(this);
66 this.setFinishOnTouchOutside(false);
67 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
68 }
69
70 @Override
71 protected void onStart() {
72 super.onStart();
73 if (!startRecording()) {
74 mStopButton.setEnabled(false);
75 Toast.makeText(this, R.string.unable_to_start_recording, Toast.LENGTH_SHORT).show();
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.HE_AAC);
96 mRecorder.setAudioEncodingBitRate(48000);
97 mRecorder.setAudioSamplingRate(16000);
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(boolean saveFile) {
115 mShouldFinishAfterWrite = saveFile;
116 mRecorder.stop();
117 mRecorder.release();
118 mRecorder = null;
119 mStartTime = 0;
120 if (!saveFile && mOutputFile != null) {
121 if (mOutputFile.delete()) {
122 Log.d(Config.LOGTAG,"deleted canceled recording");
123 }
124 }
125 }
126
127 private static File generateOutputFilename(Context context) {
128 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US);
129 String filename = "RECORDING_" + dateFormat.format(new Date()) + ".m4a";
130 return new File(FileBackend.getConversationsDirectory(context, STORAGE_DIRECTORY_TYPE_NAME) + "/" + filename);
131 }
132
133 private void setupOutputFile() {
134 mOutputFile = generateOutputFilename(this);
135 File parentDirectory = mOutputFile.getParentFile();
136 if (parentDirectory.mkdirs()) {
137 Log.d(Config.LOGTAG, "created " + parentDirectory.getAbsolutePath());
138 }
139 File noMedia = new File(parentDirectory, ".nomedia");
140 if (!noMedia.exists()) {
141 try {
142 if (noMedia.createNewFile()) {
143 Log.d(Config.LOGTAG, "created nomedia file in " + parentDirectory.getAbsolutePath());
144 }
145 } catch (IOException e) {
146 Log.d(Config.LOGTAG, "unable to create nomedia file in " + parentDirectory.getAbsolutePath(), e);
147 }
148 }
149 setupFileObserver(parentDirectory);
150 }
151
152 private void setupFileObserver(File directory) {
153 mFileObserver = new FileObserver(directory.getAbsolutePath()) {
154 @Override
155 public void onEvent(int event, String s) {
156 if (s != null && s.equals(mOutputFile.getName()) && event == FileObserver.CLOSE_WRITE) {
157 if (mShouldFinishAfterWrite) {
158 setResult(Activity.RESULT_OK, new Intent().setData(Uri.fromFile(mOutputFile)));
159 finish();
160 }
161 }
162 }
163 };
164 mFileObserver.startWatching();
165 }
166
167 private void tick() {
168 long time = (mStartTime < 0) ? 0 : (SystemClock.elapsedRealtime() - mStartTime);
169 int minutes = (int) (time / 60000);
170 int seconds = (int) (time / 1000) % 60;
171 int milliseconds = (int) (time / 100) % 10;
172 mTimerTextView.setText(minutes + ":" + (seconds < 10 ? "0" + seconds : seconds) + "." + milliseconds);
173 }
174
175 @Override
176 public void onClick(View view) {
177 switch (view.getId()) {
178 case R.id.cancel_button:
179 mHandler.removeCallbacks(mTickExecutor);
180 stopRecording(false);
181 setResult(RESULT_CANCELED);
182 finish();
183 break;
184 case R.id.share_button:
185 mStopButton.setEnabled(false);
186 mStopButton.setText(R.string.please_wait);
187 mHandler.removeCallbacks(mTickExecutor);
188 mHandler.postDelayed(() -> stopRecording(true), 500);
189 break;
190 }
191 }
192}