RecordingActivity.java

  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.FileObserver;
 10import android.os.Handler;
 11import android.os.SystemClock;
 12import android.util.Log;
 13import android.view.View;
 14import android.view.WindowManager;
 15import android.widget.Toast;
 16
 17import androidx.databinding.DataBindingUtil;
 18
 19import java.io.File;
 20import java.io.IOException;
 21import java.text.SimpleDateFormat;
 22import java.util.Date;
 23import java.util.Locale;
 24import java.util.concurrent.CountDownLatch;
 25import java.util.concurrent.TimeUnit;
 26
 27import eu.siacs.conversations.Config;
 28import eu.siacs.conversations.R;
 29import eu.siacs.conversations.databinding.ActivityRecordingBinding;
 30import eu.siacs.conversations.persistance.FileBackend;
 31import eu.siacs.conversations.utils.ThemeHelper;
 32import eu.siacs.conversations.utils.TimeFrameUtils;
 33
 34public class RecordingActivity extends Activity implements View.OnClickListener {
 35
 36    public static String STORAGE_DIRECTORY_TYPE_NAME = "Recordings";
 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 = 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 onStart() {
 71        super.onStart();
 72        if (!startRecording()) {
 73            this.binding.shareButton.setEnabled(false);
 74            this.binding.timer.setTextAppearance(this, R.style.TextAppearance_Conversations_Title);
 75            this.binding.timer.setText(R.string.unable_to_start_recording);
 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.AAC);
 96        mRecorder.setAudioEncodingBitRate(96000);
 97        mRecorder.setAudioSamplingRate(22050);
 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(final boolean saveFile) {
115        try {
116            mRecorder.stop();
117            mRecorder.release();
118        } catch (Exception e) {
119            if (saveFile) {
120                Toast.makeText(this, R.string.unable_to_save_recording, Toast.LENGTH_SHORT).show();
121                return;
122            }
123        } finally {
124            mRecorder = null;
125            mStartTime = 0;
126        }
127        if (!saveFile && mOutputFile != null) {
128            if (mOutputFile.delete()) {
129                Log.d(Config.LOGTAG, "deleted canceled recording");
130            }
131        }
132        if (saveFile) {
133            new Thread(() -> {
134                try {
135                    if (!outputFileWrittenLatch.await(2, TimeUnit.SECONDS)) {
136                        Log.d(Config.LOGTAG, "time out waiting for output file to be written");
137                    }
138                } catch (InterruptedException e) {
139                    Log.d(Config.LOGTAG, "interrupted while waiting for output file to be written", e);
140                }
141                runOnUiThread(() -> {
142                    setResult(Activity.RESULT_OK, new Intent().setData(Uri.fromFile(mOutputFile)));
143                    finish();
144                });
145            }).start();
146        }
147    }
148
149    private static File generateOutputFilename(Context context) {
150        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US);
151        String filename = "RECORDING_" + dateFormat.format(new Date()) + ".m4a";
152        return new File(FileBackend.getConversationsDirectory(context, STORAGE_DIRECTORY_TYPE_NAME) + "/" + filename);
153    }
154
155    private void setupOutputFile() {
156        mOutputFile = generateOutputFilename(this);
157        File parentDirectory = mOutputFile.getParentFile();
158        if (parentDirectory.mkdirs()) {
159            Log.d(Config.LOGTAG, "created " + parentDirectory.getAbsolutePath());
160        }
161        File noMedia = new File(parentDirectory, ".nomedia");
162        if (!noMedia.exists()) {
163            try {
164                if (noMedia.createNewFile()) {
165                    Log.d(Config.LOGTAG, "created nomedia file in " + parentDirectory.getAbsolutePath());
166                }
167            } catch (IOException e) {
168                Log.d(Config.LOGTAG, "unable to create nomedia file in " + parentDirectory.getAbsolutePath(), e);
169            }
170        }
171        setupFileObserver(parentDirectory);
172    }
173
174    private void setupFileObserver(File directory) {
175        mFileObserver = new FileObserver(directory.getAbsolutePath()) {
176            @Override
177            public void onEvent(int event, String s) {
178                if (s != null && s.equals(mOutputFile.getName()) && event == FileObserver.CLOSE_WRITE) {
179                    outputFileWrittenLatch.countDown();
180                }
181            }
182        };
183        mFileObserver.startWatching();
184    }
185
186    private void tick() {
187        this.binding.timer.setText(TimeFrameUtils.formatTimePassed(mStartTime, true));
188    }
189
190    @Override
191    public void onClick(View view) {
192        switch (view.getId()) {
193            case R.id.cancel_button:
194                mHandler.removeCallbacks(mTickExecutor);
195                stopRecording(false);
196                setResult(RESULT_CANCELED);
197                finish();
198                break;
199            case R.id.share_button:
200                this.binding.shareButton.setEnabled(false);
201                this.binding.shareButton.setText(R.string.please_wait);
202                mHandler.removeCallbacks(mTickExecutor);
203                mHandler.postDelayed(() -> stopRecording(true), 500);
204                break;
205        }
206    }
207}