RecordingActivity.java

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