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.lang.ref.WeakReference;
 22import java.text.SimpleDateFormat;
 23import java.util.Date;
 24import java.util.Locale;
 25import java.util.Objects;
 26import java.util.concurrent.CountDownLatch;
 27import java.util.concurrent.TimeUnit;
 28
 29import eu.siacs.conversations.Config;
 30import eu.siacs.conversations.R;
 31import eu.siacs.conversations.databinding.ActivityRecordingBinding;
 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    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 =
 47            new Runnable() {
 48                @Override
 49                public void run() {
 50                    tick();
 51                    mHandler.postDelayed(mTickExecutor, 100);
 52                }
 53            };
 54
 55    private File mOutputFile;
 56
 57    private FileObserver mFileObserver;
 58
 59    @Override
 60    protected void onCreate(Bundle savedInstanceState) {
 61        setTheme(ThemeHelper.findDialog(this));
 62        super.onCreate(savedInstanceState);
 63        this.binding = DataBindingUtil.setContentView(this, R.layout.activity_recording);
 64        this.binding.cancelButton.setOnClickListener(this);
 65        this.binding.shareButton.setOnClickListener(this);
 66        this.setFinishOnTouchOutside(false);
 67        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
 68    }
 69
 70    @Override
 71    protected void onResume() {
 72        super.onResume();
 73        SettingsUtils.applyScreenshotPreventionSetting(this);
 74    }
 75
 76    @Override
 77    protected void onStart() {
 78        super.onStart();
 79        if (!startRecording()) {
 80            this.binding.shareButton.setEnabled(false);
 81            this.binding.timer.setTextAppearance(this, R.style.TextAppearance_Conversations_Title);
 82            this.binding.timer.setText(R.string.unable_to_start_recording);
 83        }
 84    }
 85
 86    @Override
 87    protected void onStop() {
 88        super.onStop();
 89        if (mRecorder != null) {
 90            mHandler.removeCallbacks(mTickExecutor);
 91            stopRecording(false);
 92        }
 93        if (mFileObserver != null) {
 94            mFileObserver.stopWatching();
 95        }
 96    }
 97
 98    private boolean startRecording() {
 99        mRecorder = new MediaRecorder();
100        mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
101        final int outputFormat;
102        if (Config.USE_OPUS_VOICE_MESSAGES && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
103            outputFormat = MediaRecorder.OutputFormat.OGG;
104            mRecorder.setOutputFormat(outputFormat);
105            mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.OPUS);
106            mRecorder.setAudioEncodingBitRate(32000);
107        } else {
108            outputFormat = MediaRecorder.OutputFormat.MPEG_4;
109            mRecorder.setOutputFormat(outputFormat);
110            mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
111            mRecorder.setAudioEncodingBitRate(96000);
112            mRecorder.setAudioSamplingRate(22050);
113        }
114        setupOutputFile(outputFormat);
115        mRecorder.setOutputFile(mOutputFile.getAbsolutePath());
116
117        try {
118            mRecorder.prepare();
119            mRecorder.start();
120            mStartTime = SystemClock.elapsedRealtime();
121            mHandler.postDelayed(mTickExecutor, 100);
122            Log.d(Config.LOGTAG, "started recording to " + mOutputFile.getAbsolutePath());
123            return true;
124        } catch (Exception e) {
125            Log.e(Config.LOGTAG, "prepare() failed ", e);
126            return false;
127        }
128    }
129
130    protected void stopRecording(final boolean saveFile) {
131        try {
132            mRecorder.stop();
133            mRecorder.release();
134        } catch (Exception e) {
135            if (saveFile) {
136                Toast.makeText(this, R.string.unable_to_save_recording, Toast.LENGTH_SHORT).show();
137                return;
138            }
139        } finally {
140            mRecorder = null;
141            mStartTime = 0;
142        }
143        if (!saveFile && mOutputFile != null) {
144            if (mOutputFile.delete()) {
145                Log.d(Config.LOGTAG, "deleted canceled recording");
146            }
147        }
148        if (saveFile) {
149            new Thread(new Finisher(outputFileWrittenLatch, mOutputFile, this)).start();
150        }
151    }
152
153    private static class Finisher implements Runnable {
154
155        private final CountDownLatch latch;
156        private final File outputFile;
157        private final WeakReference<Activity> activityReference;
158
159        private Finisher(CountDownLatch latch, File outputFile, Activity activity) {
160            this.latch = latch;
161            this.outputFile = outputFile;
162            this.activityReference = new WeakReference<>(activity);
163        }
164
165        @Override
166        public void run() {
167            try {
168                if (!latch.await(8, TimeUnit.SECONDS)) {
169                    Log.d(Config.LOGTAG, "time out waiting for output file to be written");
170                }
171            } catch (final InterruptedException e) {
172                Log.d(Config.LOGTAG, "interrupted while waiting for output file to be written", e);
173            }
174            final Activity activity = activityReference.get();
175            if (activity == null) {
176                return;
177            }
178            activity.runOnUiThread(
179                    () -> {
180                        activity.setResult(
181                                Activity.RESULT_OK, new Intent().setData(Uri.fromFile(outputFile)));
182                        activity.finish();
183                    });
184        }
185    }
186
187    private File generateOutputFilename(final int outputFormat) {
188        final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US);
189        final String extension;
190        if (outputFormat == MediaRecorder.OutputFormat.MPEG_4) {
191            extension = "m4a";
192        } else if (outputFormat == MediaRecorder.OutputFormat.OGG) {
193            extension = "oga";
194        } else {
195            throw new IllegalStateException("Unrecognized output format");
196        }
197        final String filename =
198                String.format("RECORDING_%s.%s", dateFormat.format(new Date()), extension);
199        final File parentDirectory;
200        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
201            parentDirectory =
202                    Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_RECORDINGS);
203        } else {
204            parentDirectory =
205                    Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
206        }
207        final File conversationsDirectory = new File(parentDirectory, getString(R.string.app_name));
208        return new File(conversationsDirectory, filename);
209    }
210
211    private void setupOutputFile(final int outputFormat) {
212        mOutputFile = generateOutputFilename(outputFormat);
213        final File parentDirectory = mOutputFile.getParentFile();
214        if (Objects.requireNonNull(parentDirectory).mkdirs()) {
215            Log.d(Config.LOGTAG, "created " + parentDirectory.getAbsolutePath());
216        }
217        setupFileObserver(parentDirectory);
218    }
219
220    private void setupFileObserver(final File directory) {
221        mFileObserver =
222                new FileObserver(directory.getAbsolutePath()) {
223                    @Override
224                    public void onEvent(int event, String s) {
225                        if (s != null
226                                && s.equals(mOutputFile.getName())
227                                && event == FileObserver.CLOSE_WRITE) {
228                            outputFileWrittenLatch.countDown();
229                        }
230                    }
231                };
232        mFileObserver.startWatching();
233    }
234
235    private void tick() {
236        this.binding.timer.setText(TimeFrameUtils.formatTimePassed(mStartTime, true));
237    }
238
239    @Override
240    public void onClick(final View view) {
241        switch (view.getId()) {
242            case R.id.cancel_button:
243                mHandler.removeCallbacks(mTickExecutor);
244                stopRecording(false);
245                setResult(RESULT_CANCELED);
246                finish();
247                break;
248            case R.id.share_button:
249                this.binding.shareButton.setEnabled(false);
250                this.binding.shareButton.setText(R.string.please_wait);
251                mHandler.removeCallbacks(mTickExecutor);
252                mHandler.postDelayed(() -> stopRecording(true), 500);
253                break;
254        }
255    }
256}