RecordingActivity.java

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