|
| 1 | +package org.telegram.messenger; |
| 2 | + |
| 3 | +import android.content.Context; |
| 4 | +import android.os.Handler; |
| 5 | +import android.os.Looper; |
| 6 | + |
| 7 | +import androidx.annotation.NonNull; |
| 8 | +import androidx.annotation.Nullable; |
| 9 | + |
| 10 | +import org.telegram.utils.recorder.audio.AudioRecordingSession; |
| 11 | +import org.telegram.utils.settings.SharedSettings; |
| 12 | + |
| 13 | +import java.io.File; |
| 14 | +import java.util.HashMap; |
| 15 | +import java.util.HashSet; |
| 16 | + |
| 17 | +/** Bridges the standalone recorder to Telegram's existing voice-recording lifecycle. */ |
| 18 | +final class AudioRecorderV2Controller { |
| 19 | + |
| 20 | + interface Listener { |
| 21 | + void onOutputStarted(@NonNull File file); |
| 22 | + |
| 23 | + void onOutputInvalidated(@NonNull File file); |
| 24 | + |
| 25 | + void onRecordingStarted(); |
| 26 | + |
| 27 | + void onRecordingResumed(); |
| 28 | + |
| 29 | + void onRecordingProgress(long durationMs, double amplitude); |
| 30 | + |
| 31 | + void onPreviewReady( |
| 32 | + long durationMs, |
| 33 | + @NonNull File file, |
| 34 | + @Nullable byte[] waveform |
| 35 | + ); |
| 36 | + |
| 37 | + void onCompleted(@NonNull AudioRecordingSession.Result result); |
| 38 | + |
| 39 | + void onRecordingTooShort(long durationMs); |
| 40 | + |
| 41 | + void onError(@NonNull Exception error); |
| 42 | + } |
| 43 | + |
| 44 | + private final Handler mainHandler = new Handler(Looper.getMainLooper()); |
| 45 | + private final Listener listener; |
| 46 | + private final AudioRecordingSession session; |
| 47 | + private final Object outputLock = new Object(); |
| 48 | + private final HashMap<Long, File> outputFiles = new HashMap<>(); |
| 49 | + private final HashSet<Long> deliveredOutputIds = new HashSet<>(); |
| 50 | + |
| 51 | + private AudioRecordingSession.State previousState = AudioRecordingSession.State.IDLE; |
| 52 | + private boolean finishRequested; |
| 53 | + private boolean released; |
| 54 | + |
| 55 | + AudioRecorderV2Controller( |
| 56 | + @NonNull Context context, |
| 57 | + @NonNull File outputDirectory, |
| 58 | + @NonNull Listener listener |
| 59 | + ) { |
| 60 | + this.listener = listener; |
| 61 | + session = new AudioRecordingSession.Builder(context) |
| 62 | + .setOutputDirectory(outputDirectory) |
| 63 | + .setMinimumDurationMs(700L) |
| 64 | + .setMaximumDurationMs(0L) |
| 65 | + .setNoiseSuppression(SharedSettings.audioRecorderNoiseSuppression.get()) |
| 66 | + .setListener(new AudioRecordingSession.Listener() { |
| 67 | + @Override |
| 68 | + public void onStateChanged(@NonNull AudioRecordingSession.StateInfo stateInfo) { |
| 69 | + AudioRecordingSession.State state = stateInfo.getState(); |
| 70 | + AudioRecordingSession.State previous = previousState; |
| 71 | + previousState = state; |
| 72 | + if (state == AudioRecordingSession.State.RECORDING) { |
| 73 | + if (!finishRequested |
| 74 | + && previous == AudioRecordingSession.State.STARTING) { |
| 75 | + listener.onRecordingStarted(); |
| 76 | + } else if (!finishRequested |
| 77 | + && previous == AudioRecordingSession.State.RESUMING) { |
| 78 | + listener.onRecordingResumed(); |
| 79 | + } |
| 80 | + completePendingFinish(state); |
| 81 | + } |
| 82 | + } |
| 83 | + |
| 84 | + @Override |
| 85 | + public void onRecordingProgress(long durationMs, double amplitude) { |
| 86 | + listener.onRecordingProgress(durationMs, amplitude); |
| 87 | + } |
| 88 | + |
| 89 | + @Override |
| 90 | + public void onPreviewReady( |
| 91 | + long durationMs, |
| 92 | + long trimStartMs, |
| 93 | + long trimEndMs, |
| 94 | + @Nullable byte[] waveform |
| 95 | + ) { |
| 96 | + if (finishRequested) { |
| 97 | + finishRequested = false; |
| 98 | + session.finish(); |
| 99 | + return; |
| 100 | + } |
| 101 | + File file = session.getPreviewFile(); |
| 102 | + if (file == null) { |
| 103 | + listener.onError(new IllegalStateException("Preview file is unavailable")); |
| 104 | + return; |
| 105 | + } |
| 106 | + listener.onPreviewReady(durationMs, file, waveform); |
| 107 | + } |
| 108 | + |
| 109 | + @Override |
| 110 | + public void onRecordingTooShort(long durationMs) { |
| 111 | + listener.onRecordingTooShort(durationMs); |
| 112 | + } |
| 113 | + |
| 114 | + @Override |
| 115 | + public void onCompleted(@NonNull AudioRecordingSession.Result result) { |
| 116 | + listener.onCompleted(result); |
| 117 | + } |
| 118 | + |
| 119 | + @Override |
| 120 | + public void onError(@NonNull Exception error) { |
| 121 | + listener.onError(error); |
| 122 | + } |
| 123 | + }) |
| 124 | + .setOutputListener(new AudioRecordingSession.OutputListener() { |
| 125 | + @Override |
| 126 | + public void onOutputStarted(long outputId, @NonNull File file) { |
| 127 | + synchronized (outputLock) { |
| 128 | + outputFiles.put(outputId, file); |
| 129 | + } |
| 130 | + mainHandler.post(() -> { |
| 131 | + if (released) return; |
| 132 | + deliveredOutputIds.add(outputId); |
| 133 | + listener.onOutputStarted(file); |
| 134 | + }); |
| 135 | + } |
| 136 | + |
| 137 | + @Override |
| 138 | + public void onOutputInvalidated( |
| 139 | + long outputId, |
| 140 | + @NonNull AudioRecordingSession.OutputInvalidationReason reason |
| 141 | + ) { |
| 142 | + File file; |
| 143 | + synchronized (outputLock) { |
| 144 | + file = outputFiles.remove(outputId); |
| 145 | + } |
| 146 | + if (file != null) { |
| 147 | + mainHandler.post(() -> { |
| 148 | + if (deliveredOutputIds.remove(outputId)) { |
| 149 | + listener.onOutputInvalidated(file); |
| 150 | + } |
| 151 | + }); |
| 152 | + } |
| 153 | + } |
| 154 | + |
| 155 | + @Override |
| 156 | + public void onOutputCompleted( |
| 157 | + long outputId, |
| 158 | + @NonNull File file, |
| 159 | + long finalSize, |
| 160 | + long durationMs |
| 161 | + ) { |
| 162 | + synchronized (outputLock) { |
| 163 | + outputFiles.put(outputId, file); |
| 164 | + } |
| 165 | + } |
| 166 | + }) |
| 167 | + .setLogger(new AudioRecordingSession.Logger() { |
| 168 | + @Override |
| 169 | + public void log(@NonNull String message) { |
| 170 | + if (BuildVars.LOGS_ENABLED) { |
| 171 | + FileLog.d("AudioRecorderV2 " + message); |
| 172 | + } |
| 173 | + } |
| 174 | + |
| 175 | + @Override |
| 176 | + public void error(@NonNull String message, @NonNull Throwable error) { |
| 177 | + FileLog.e("AudioRecorderV2 " + message, error); |
| 178 | + } |
| 179 | + }) |
| 180 | + .build(); |
| 181 | + } |
| 182 | + |
| 183 | + boolean start() { |
| 184 | + return session.start(); |
| 185 | + } |
| 186 | + |
| 187 | + boolean togglePause() { |
| 188 | + AudioRecordingSession.State state = session.getStateInfo().getState(); |
| 189 | + if (state == AudioRecordingSession.State.RECORDING) { |
| 190 | + return session.pauseRecording(); |
| 191 | + } |
| 192 | + if (state == AudioRecordingSession.State.PREVIEWING) { |
| 193 | + return session.resumeRecording(); |
| 194 | + } |
| 195 | + return false; |
| 196 | + } |
| 197 | + |
| 198 | + boolean setTrimRange(long startMs, long endMs) { |
| 199 | + return session.setTrimRange(startMs, endMs); |
| 200 | + } |
| 201 | + |
| 202 | + void finishWhenReady() { |
| 203 | + finishRequested = true; |
| 204 | + completePendingFinish(session.getStateInfo().getState()); |
| 205 | + } |
| 206 | + |
| 207 | + void cancel() { |
| 208 | + finishRequested = false; |
| 209 | + session.cancel(); |
| 210 | + } |
| 211 | + |
| 212 | + void release() { |
| 213 | + session.release(); |
| 214 | + released = true; |
| 215 | + } |
| 216 | + |
| 217 | + boolean detachPreviewOutput() { |
| 218 | + AudioRecordingSession.Result result = session.detachPreviewOutput(); |
| 219 | + if (result == null) return false; |
| 220 | + session.release(); |
| 221 | + released = true; |
| 222 | + return true; |
| 223 | + } |
| 224 | + |
| 225 | + boolean isPreviewing() { |
| 226 | + return session.getStateInfo().getState() == AudioRecordingSession.State.PREVIEWING; |
| 227 | + } |
| 228 | + |
| 229 | + private void completePendingFinish(@NonNull AudioRecordingSession.State state) { |
| 230 | + if (!finishRequested) return; |
| 231 | + if (state == AudioRecordingSession.State.RECORDING |
| 232 | + || state == AudioRecordingSession.State.PREVIEWING) { |
| 233 | + finishRequested = false; |
| 234 | + session.finish(); |
| 235 | + } |
| 236 | + } |
| 237 | +} |
0 commit comments