RecordingActivity.java

  1package eu.siacs.conversations.ui;
  2
  3import android.app.Activity;
  4import android.content.Intent;
  5import android.media.MediaRecorder;
  6import android.net.Uri;
  7import android.os.Build;
  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.Toast;
 17
 18import androidx.databinding.DataBindingUtil;
 19
 20import java.io.File;
 21import java.text.SimpleDateFormat;
 22import java.util.Date;
 23import java.util.Locale;
 24import java.util.Objects;
 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.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    private ActivityRecordingBinding binding;
 38
 39    private MediaRecorder mRecorder;
 40    private long mStartTime = 0;
 41
 42    private final CountDownLatch outputFileWrittenLatch = new CountDownLatch(1);
 43
 44    private final Handler mHandler = new Handler();
 45    private final Runnable mTickExecutor =
 46            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 onResume() {
 71        super.onResume();
 72        SettingsUtils.applyScreenshotPreventionSetting(this);
 73    }
 74
 75    @Override
 76    protected void onStart() {
 77        super.onStart();
 78        if (!startRecording()) {
 79            this.binding.shareButton.setEnabled(false);
 80            this.binding.timer.setTextAppearance(this, R.style.TextAppearance_Conversations_Title);
 81            this.binding.timer.setText(R.string.unable_to_start_recording);
 82        }
 83    }
 84
 85    @Override
 86    protected void onStop() {
 87        super.onStop();
 88        if (mRecorder != null) {
 89            mHandler.removeCallbacks(mTickExecutor);
 90            stopRecording(false);
 91        }
 92        if (mFileObserver != null) {
 93            mFileObserver.stopWatching();
 94        }
 95    }
 96
 97    private boolean startRecording() {
 98        mRecorder = new MediaRecorder();
 99        mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
100        mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
101        mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
102        mRecorder.setAudioEncodingBitRate(96000);
103        mRecorder.setAudioSamplingRate(22050);
104        setupOutputFile();
105        mRecorder.setOutputFile(mOutputFile.getAbsolutePath());
106
107        try {
108            mRecorder.prepare();
109            mRecorder.start();
110            mStartTime = SystemClock.elapsedRealtime();
111            mHandler.postDelayed(mTickExecutor, 100);
112            Log.d("Voice Recorder", "started recording to " + mOutputFile.getAbsolutePath());
113            return true;
114        } catch (Exception e) {
115            Log.e("Voice Recorder", "prepare() failed " + e.getMessage());
116            return false;
117        }
118    }
119
120    protected void stopRecording(final boolean saveFile) {
121        try {
122            mRecorder.stop();
123            mRecorder.release();
124        } catch (Exception e) {
125            if (saveFile) {
126                Toast.makeText(this, R.string.unable_to_save_recording, Toast.LENGTH_SHORT).show();
127                return;
128            }
129        } finally {
130            mRecorder = null;
131            mStartTime = 0;
132        }
133        if (!saveFile && mOutputFile != null) {
134            if (mOutputFile.delete()) {
135                Log.d(Config.LOGTAG, "deleted canceled recording");
136            }
137        }
138        if (saveFile) {
139            new Thread(
140                            () -> {
141                                try {
142                                    if (!outputFileWrittenLatch.await(2, TimeUnit.SECONDS)) {
143                                        Log.d(
144                                                Config.LOGTAG,
145                                                "time out waiting for output file to be written");
146                                    }
147                                } catch (InterruptedException e) {
148                                    Log.d(
149                                            Config.LOGTAG,
150                                            "interrupted while waiting for output file to be written",
151                                            e);
152                                }
153                                runOnUiThread(
154                                        () -> {
155                                            setResult(
156                                                    Activity.RESULT_OK,
157                                                    new Intent()
158                                                            .setData(Uri.fromFile(mOutputFile)));
159                                            finish();
160                                        });
161                            })
162                    .start();
163        }
164    }
165
166    private File generateOutputFilename() {
167        final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US);
168        final String filename = "RECORDING_" + dateFormat.format(new Date()) + ".m4a";
169        final File parentDirectory;
170        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
171            parentDirectory =
172                    Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_RECORDINGS);
173        } else {
174            parentDirectory =
175                    Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
176        }
177        final File conversationsDirectory = new File(parentDirectory, getString(R.string.app_name));
178        return new File(conversationsDirectory, filename);
179    }
180
181    private void setupOutputFile() {
182        mOutputFile = generateOutputFilename();
183        final File parentDirectory = mOutputFile.getParentFile();
184        if (Objects.requireNonNull(parentDirectory).mkdirs()) {
185            Log.d(Config.LOGTAG, "created " + parentDirectory.getAbsolutePath());
186        }
187        setupFileObserver(parentDirectory);
188    }
189
190    private void setupFileObserver(File directory) {
191        mFileObserver =
192                new FileObserver(directory.getAbsolutePath()) {
193                    @Override
194                    public void onEvent(int event, String s) {
195                        if (s != null
196                                && s.equals(mOutputFile.getName())
197                                && event == FileObserver.CLOSE_WRITE) {
198                            outputFileWrittenLatch.countDown();
199                        }
200                    }
201                };
202        mFileObserver.startWatching();
203    }
204
205    private void tick() {
206        this.binding.timer.setText(TimeFrameUtils.formatTimePassed(mStartTime, true));
207    }
208
209    @Override
210    public void onClick(View view) {
211        switch (view.getId()) {
212            case R.id.cancel_button:
213                mHandler.removeCallbacks(mTickExecutor);
214                stopRecording(false);
215                setResult(RESULT_CANCELED);
216                finish();
217                break;
218            case R.id.share_button:
219                this.binding.shareButton.setEnabled(false);
220                this.binding.shareButton.setText(R.string.please_wait);
221                mHandler.removeCallbacks(mTickExecutor);
222                mHandler.postDelayed(() -> stopRecording(true), 500);
223                break;
224        }
225    }
226}