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