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.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    public static String STORAGE_DIRECTORY_TYPE_NAME = "Recordings";
 38
 39    private ActivityRecordingBinding binding;
 40
 41    private MediaRecorder mRecorder;
 42    private long mStartTime = 0;
 43
 44    private final CountDownLatch outputFileWrittenLatch = new CountDownLatch(1);
 45
 46    private final Handler mHandler = new Handler();
 47    private final Runnable mTickExecutor = 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        mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
102        mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
103        mRecorder.setAudioEncodingBitRate(96000);
104        mRecorder.setAudioSamplingRate(22050);
105        setupOutputFile();
106        mRecorder.setOutputFile(mOutputFile.getAbsolutePath());
107
108        try {
109            mRecorder.prepare();
110            mRecorder.start();
111            mStartTime = SystemClock.elapsedRealtime();
112            mHandler.postDelayed(mTickExecutor, 100);
113            Log.d("Voice Recorder", "started recording to " + mOutputFile.getAbsolutePath());
114            return true;
115        } catch (Exception e) {
116            Log.e("Voice Recorder", "prepare() failed " + e.getMessage());
117            return false;
118        }
119    }
120
121    protected void stopRecording(final boolean saveFile) {
122        try {
123            mRecorder.stop();
124            mRecorder.release();
125        } catch (Exception e) {
126            if (saveFile) {
127                Toast.makeText(this, R.string.unable_to_save_recording, Toast.LENGTH_SHORT).show();
128                return;
129            }
130        } finally {
131            mRecorder = null;
132            mStartTime = 0;
133        }
134        if (!saveFile && mOutputFile != null) {
135            if (mOutputFile.delete()) {
136                Log.d(Config.LOGTAG, "deleted canceled recording");
137            }
138        }
139        if (saveFile) {
140            new Thread(() -> {
141                try {
142                    if (!outputFileWrittenLatch.await(2, TimeUnit.SECONDS)) {
143                        Log.d(Config.LOGTAG, "time out waiting for output file to be written");
144                    }
145                } catch (InterruptedException e) {
146                    Log.d(Config.LOGTAG, "interrupted while waiting for output file to be written", e);
147                }
148                runOnUiThread(() -> {
149                    setResult(Activity.RESULT_OK, new Intent().setData(Uri.fromFile(mOutputFile)));
150                    finish();
151                });
152            }).start();
153        }
154    }
155
156    private static File generateOutputFilename(Context context) {
157        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US);
158        String filename = "RECORDING_" + dateFormat.format(new Date()) + ".m4a";
159        return new File(FileBackend.getConversationsDirectory(context, STORAGE_DIRECTORY_TYPE_NAME) + "/" + filename);
160    }
161
162    private void setupOutputFile() {
163        mOutputFile = generateOutputFilename(this);
164        File parentDirectory = mOutputFile.getParentFile();
165        if (parentDirectory.mkdirs()) {
166            Log.d(Config.LOGTAG, "created " + parentDirectory.getAbsolutePath());
167        }
168        File noMedia = new File(parentDirectory, ".nomedia");
169        if (!noMedia.exists()) {
170            try {
171                if (noMedia.createNewFile()) {
172                    Log.d(Config.LOGTAG, "created nomedia file in " + parentDirectory.getAbsolutePath());
173                }
174            } catch (IOException e) {
175                Log.d(Config.LOGTAG, "unable to create nomedia file in " + parentDirectory.getAbsolutePath(), e);
176            }
177        }
178        setupFileObserver(parentDirectory);
179    }
180
181    private void setupFileObserver(File directory) {
182        mFileObserver = new FileObserver(directory.getAbsolutePath()) {
183            @Override
184            public void onEvent(int event, String s) {
185                if (s != null && s.equals(mOutputFile.getName()) && event == FileObserver.CLOSE_WRITE) {
186                    outputFileWrittenLatch.countDown();
187                }
188            }
189        };
190        mFileObserver.startWatching();
191    }
192
193    private void tick() {
194        this.binding.timer.setText(TimeFrameUtils.formatTimePassed(mStartTime, true));
195    }
196
197    @Override
198    public void onClick(View view) {
199        switch (view.getId()) {
200            case R.id.cancel_button:
201                mHandler.removeCallbacks(mTickExecutor);
202                stopRecording(false);
203                setResult(RESULT_CANCELED);
204                finish();
205                break;
206            case R.id.share_button:
207                this.binding.shareButton.setEnabled(false);
208                this.binding.shareButton.setText(R.string.please_wait);
209                mHandler.removeCallbacks(mTickExecutor);
210                mHandler.postDelayed(() -> stopRecording(true), 500);
211                break;
212        }
213    }
214}