diff --git a/app/jni/voice.c b/app/jni/voice.c index b14bac1673..53a55de485 100644 --- a/app/jni/voice.c +++ b/app/jni/voice.c @@ -80,6 +80,20 @@ typedef struct { int copy_comments; } oe_enc_opt; +typedef struct { + ogg_int32_t _packetId; + opus_int64 bytes_written; + opus_int64 pages_out; + opus_int64 total_samples; + ogg_int64_t enc_granulepos; + int size_segments; + int last_segments; + ogg_int64_t last_granulepos; + opus_int32 min_bytes; + int max_frame_bytes; + int serialno; +} resume_data; + static int write_uint32(Packet *p, ogg_uint32_t val) { if (p->pos > p->maxlen - 4) { return 0; @@ -248,18 +262,19 @@ static int writeOggPage(ogg_page *page, FILE *os) { return written; } -const opus_int32 bitrate = 32000; -const opus_int32 rate = 48000; +const opus_int32 bitrate = OPUS_BITRATE_MAX; const opus_int32 frame_size = 960; const int with_cvbr = 1; const int max_ogg_delay = 0; const int comment_padding = 512; +opus_int32 rate = 48000; opus_int32 coding_rate = 48000; ogg_int32_t _packetId; OpusEncoder *_encoder = 0; uint8_t *_packet = 0; ogg_stream_state os; +char *_filePath = NULL; FILE *_fileOs = 0; oe_enc_opt inopt; OpusHeader header; @@ -274,6 +289,7 @@ ogg_int64_t enc_granulepos; ogg_int64_t last_granulepos; int size_segments; int last_segments; +int serialno; void cleanupRecorder() { @@ -304,6 +320,10 @@ void cleanupRecorder() { size_segments = 0; last_segments = 0; last_granulepos = 0; + if (_filePath) { + free(_filePath); + _filePath = NULL; + } memset(&os, 0, sizeof(ogg_stream_state)); memset(&inopt, 0, sizeof(oe_enc_opt)); memset(&header, 0, sizeof(OpusHeader)); @@ -311,15 +331,24 @@ void cleanupRecorder() { memset(&og, 0, sizeof(ogg_page)); } -int initRecorder(const char *path) { +int initRecorder(const char *path, opus_int32 sampleRate) { cleanupRecorder(); + coding_rate = sampleRate; + rate = sampleRate; + if (!path) { + loge(TAG_VOICE, "path is null"); return 0; } - _fileOs = fopen(path, "wb"); + int length = strlen(path); + _filePath = (char*) malloc(length + 1); + strcpy(_filePath, path); + + _fileOs = fopen(path, "w"); if (!_fileOs) { + loge(TAG_VOICE, "error cannot open file: %s", path); return 0; } @@ -327,26 +356,14 @@ int initRecorder(const char *path) { inopt.gain = 0; inopt.endianness = 0; inopt.copy_comments = 0; - inopt.rawmode = 1; - inopt.ignorelength = 1; + inopt.rawmode = 0; + inopt.ignorelength = 0; inopt.samplesize = 16; inopt.channels = 1; inopt.skip = 0; comment_init(&inopt.comments, &inopt.comments_length, opus_get_version_string()); - if (rate > 24000) { - coding_rate = 48000; - } else if (rate > 16000) { - coding_rate = 24000; - } else if (rate > 12000) { - coding_rate = 16000; - } else if (rate > 8000) { - coding_rate = 12000; - } else { - coding_rate = 8000; - } - if (rate != coding_rate) { loge(TAG_VOICE, "Invalid rate"); return 0; @@ -369,6 +386,7 @@ int initRecorder(const char *path) { _packet = malloc(max_frame_bytes); result = opus_encoder_ctl(_encoder, OPUS_SET_BITRATE(bitrate)); + //result = opus_encoder_ctl(_encoder, OPUS_SET_COMPLEXITY(10)); if (result != OPUS_OK) { loge(TAG_VOICE, "Error OPUS_SET_BITRATE returned: %s", opus_strerror(result)); return 0; @@ -392,7 +410,7 @@ int initRecorder(const char *path) { header.preskip = (int)(inopt.skip * (48000.0 / coding_rate)); inopt.extraout = (int)(header.preskip * (rate / 48000.0)); - if (ogg_stream_init(&os, rand()) == -1) { + if (ogg_stream_init(&os, serialno = rand()) == -1) { loge(TAG_VOICE, "Error: stream init failed"); return 0; } @@ -450,6 +468,135 @@ int initRecorder(const char *path) { return 1; } +void saveResumeData() { + if (_filePath == NULL) { + return; + } + const char* ext = ".resume"; + char* _resumeFilePath = (char*) malloc(strlen(_filePath) + strlen(ext) + 1); + strcpy(_resumeFilePath, _filePath); + strcat(_resumeFilePath, ext); + + FILE* resumeFile = fopen(_resumeFilePath, "wb"); + if (!resumeFile) { + loge(TAG_VOICE, "error cannot open resume file to write: %s", _resumeFilePath); + free(_resumeFilePath); + return; + } + resume_data data; + data._packetId = _packetId; + data.bytes_written = bytes_written; + data.pages_out = pages_out; + data.total_samples = total_samples; + data.enc_granulepos = enc_granulepos; + data.size_segments = size_segments; + data.last_segments = last_segments; + data.last_granulepos = last_granulepos; + data.min_bytes = min_bytes; + data.max_frame_bytes = max_frame_bytes; + data.serialno = serialno; + + if (fwrite(&data, sizeof(resume_data), 1, resumeFile) != 1) { + loge(TAG_VOICE, "error writing resume data to file: %s", _resumeFilePath); + } + fclose(resumeFile); + + free(_resumeFilePath); +} + +resume_data readResumeData(const char* filePath) { + + const char* ext = ".resume"; + char* _resumeFilePath = (char*) malloc(strlen(filePath) + strlen(ext) + 1); + strcpy(_resumeFilePath, filePath); + strcat(_resumeFilePath, ext); + + resume_data data; + + FILE* resumeFile = fopen(_resumeFilePath, "rb"); + if (!resumeFile) { + loge(TAG_VOICE, "error cannot open resume file to read: %s", _resumeFilePath); + memset(&data, 0, sizeof(resume_data)); + free(_resumeFilePath); + return data; + } + + if (fread(&data, sizeof(resume_data), 1, resumeFile) != 1) { + loge(TAG_VOICE, "error cannot read resume file: %s", _resumeFilePath); + memset(&data, 0, sizeof(resume_data)); + } + + fclose(resumeFile); + free(_resumeFilePath); + + return data; +} + +int resumeRecorder(const char *path, opus_int32 sampleRate) { + cleanupRecorder(); + + coding_rate = sampleRate; + rate = sampleRate; + + if (!path) { + loge("path is null"); + return 0; + } + + int length = strlen(path); + _filePath = (char*) malloc(length + 1); + strcpy(_filePath, path); + + resume_data resumeData = readResumeData(path); + _packetId = resumeData._packetId; + bytes_written = resumeData.bytes_written; + pages_out = resumeData.pages_out; + total_samples = resumeData.total_samples; + enc_granulepos = resumeData.enc_granulepos; + size_segments = resumeData.size_segments; + last_segments = resumeData.last_segments; + last_granulepos = resumeData.last_granulepos; + min_bytes = resumeData.min_bytes; + max_frame_bytes = resumeData.max_frame_bytes; + serialno = resumeData.serialno; + + _fileOs = fopen(path, "a"); + if (!_fileOs) { + loge(TAG_VOICE, "error cannot open resume file: %s", path); + return 0; + } + + int result = OPUS_OK; + _encoder = opus_encoder_create(coding_rate, 1, OPUS_APPLICATION_VOIP, &result); + if (result != OPUS_OK) { + loge(TAG_VOICE, "Error cannot create encoder: %s", opus_strerror(result)); + return 0; + } + + _packet = malloc(max_frame_bytes); + + result = opus_encoder_ctl(_encoder, OPUS_SET_BITRATE(bitrate)); + //result = opus_encoder_ctl(_encoder, OPUS_SET_COMPLEXITY(10)); + if (result != OPUS_OK) { + loge(TAG_VOICE, "Error OPUS_SET_BITRATE returned: %s", opus_strerror(result)); + return 0; + } + +#ifdef OPUS_SET_LSB_DEPTH + result = opus_encoder_ctl(_encoder, OPUS_SET_LSB_DEPTH(max(8, min(24, 16)))); + if (result != OPUS_OK) { + loge(TAG_VOICE, "Warning OPUS_SET_LSB_DEPTH returned: %s", opus_strerror(result)); + } +#endif + + if (ogg_stream_init(&os, serialno) == -1) { + loge(TAG_VOICE, "Error: stream init failed"); + return 0; + } + + return 1; +} + int writeFrame(uint8_t *framePcmBytes, unsigned int frameByteCount) { int cur_frame_size = frame_size; _packetId++; @@ -535,10 +682,10 @@ int writeFrame(uint8_t *framePcmBytes, unsigned int frameByteCount) { return 1; } -JNIEXPORT int Java_org_thunderdog_challegram_N_startRecord(JNIEnv *env, jclass class, jstring path) { +JNIEXPORT jint Java_org_thunderdog_challegram_N_startRecord(JNIEnv *env, jclass class, jstring path, jint sampleRate) { const char *pathStr = (*env)->GetStringUTFChars(env, path, 0); - int result = initRecorder(pathStr); + int32_t result = initRecorder(pathStr, sampleRate); if (pathStr != 0) { (*env)->ReleaseStringUTFChars(env, path, pathStr); @@ -547,12 +694,27 @@ JNIEXPORT int Java_org_thunderdog_challegram_N_startRecord(JNIEnv *env, jclass c return result; } -JNIEXPORT int Java_org_thunderdog_challegram_N_writeFrame(JNIEnv *env, jclass class, jobject frame, jint len) { +JNIEXPORT jint Java_org_thunderdog_challegram_N_resumeRecord(JNIEnv *env, jclass class, jstring path, jint sampleRate) { + const char *pathStr = (*env)->GetStringUTFChars(env, path, 0); + + int32_t result = resumeRecorder(pathStr, sampleRate); + + if (pathStr != 0) { + (*env)->ReleaseStringUTFChars(env, path, pathStr); + } + + return result; +} + +JNIEXPORT jint Java_org_thunderdog_challegram_N_writeFrame(JNIEnv *env, jclass class, jobject frame, jint len) { jbyte *frameBytes = (*env)->GetDirectBufferAddress(env, frame); return writeFrame((uint8_t *) frameBytes, (size_t) len); } -JNIEXPORT void Java_org_thunderdog_challegram_N_stopRecord(JNIEnv *env, jclass class) { +JNIEXPORT void Java_org_thunderdog_challegram_N_stopRecord(JNIEnv *env, jclass class, jboolean allowResuming) { + if (allowResuming && _filePath != NULL) { + saveResumeData(); + } cleanupRecorder(); } @@ -663,11 +825,11 @@ JNIEXPORT void Java_org_thunderdog_challegram_N_readOpusFile(JNIEnv *env, jclass (*env)->ReleaseIntArrayElements(env, args, argsArr, 0); } -JNIEXPORT int Java_org_thunderdog_challegram_N_seekOpusFile(JNIEnv *env, jclass class, jfloat position) { +JNIEXPORT jint Java_org_thunderdog_challegram_N_seekOpusFile(JNIEnv *env, jclass class, jfloat position) { return seekPlayer(position); } -JNIEXPORT int Java_org_thunderdog_challegram_N_openOpusFile(JNIEnv *env, jclass class, jstring path) { +JNIEXPORT jint Java_org_thunderdog_challegram_N_openOpusFile(JNIEnv *env, jclass class, jstring path) { const char *pathStr = (*env)->GetStringUTFChars(env, path, 0); int result = initPlayer(pathStr); @@ -679,7 +841,7 @@ JNIEXPORT int Java_org_thunderdog_challegram_N_openOpusFile(JNIEnv *env, jclass return result; } -JNIEXPORT int Java_org_thunderdog_challegram_N_isOpusFile(JNIEnv *env, jclass class, jstring path) { +JNIEXPORT jint Java_org_thunderdog_challegram_N_isOpusFile(JNIEnv *env, jclass class, jstring path) { const char *pathStr = (*env)->GetStringUTFChars(env, path, 0); int result = 0; diff --git a/app/src/main/java/org/thunderdog/challegram/N.java b/app/src/main/java/org/thunderdog/challegram/N.java index 8503e9651c..07b4b85f89 100644 --- a/app/src/main/java/org/thunderdog/challegram/N.java +++ b/app/src/main/java/org/thunderdog/challegram/N.java @@ -81,9 +81,22 @@ public static int pinBitmapIfNeeded (Bitmap bitmap) { // TODO remove rendering, because it is no longer used // audio.c - public static native int startRecord (String path); + public static int startRecord (String path) { + return startRecord(path, 48000); + } + + public static int resumeRecord (String path) { + return resumeRecord(path, 48000); + } + + public static void stopRecord () { + stopRecord(false); + } + + public static native int startRecord (String path, int sampleRate); + public static native int resumeRecord (String path, int sampleRate); public static native int writeFrame (ByteBuffer frame, int len); - public static native void stopRecord (); + public static native void stopRecord (boolean allowResuming); public static native int openOpusFile (String path); public static native int seekOpusFile (float position); public static native int isOpusFile (String path); diff --git a/app/src/main/java/org/thunderdog/challegram/U.java b/app/src/main/java/org/thunderdog/challegram/U.java index 26221712ec..e9341bd41c 100644 --- a/app/src/main/java/org/thunderdog/challegram/U.java +++ b/app/src/main/java/org/thunderdog/challegram/U.java @@ -112,6 +112,7 @@ import org.thunderdog.challegram.loader.ImageReader; import org.thunderdog.challegram.loader.ImageStrictCache; import org.thunderdog.challegram.mediaview.data.MediaItem; +import org.thunderdog.challegram.telegram.RandomAccessDataSource; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.telegram.TdlibDataSource; import org.thunderdog.challegram.telegram.TdlibDelegate; @@ -756,6 +757,10 @@ public static MediaSource newMediaSource (int accountId, @Nullable TdApi.File fi } } + public static MediaSource newMediaSource (RandomAccessFile file) { + return new ProgressiveMediaSource.Factory(new RandomAccessDataSource.Factory(file)).createMediaSource(newMediaItem(Uri.EMPTY)); + } + public static MediaSource newMediaSource (int accountId, int fileId) { return new ProgressiveMediaSource.Factory(new TdlibDataSource.Factory()).createMediaSource(newMediaItem(TdlibDataSource.UriFactory.create(accountId, fileId))); } @@ -1848,7 +1853,7 @@ private static boolean moveDir (File fromDir, File toDir) { return successCount == innerFiles.length && fromDir.delete(); } - private static boolean moveFile (File fromFile, File toFile) { + public static boolean moveFile (File fromFile, File toFile) { if (fromFile.renameTo(toFile)) { return true; } @@ -3490,6 +3495,16 @@ public static long getTotalExternalMemorySize () { } } + public static int getStreamVolume (int stream) { + final AudioManager audioManager = (AudioManager) UI.getContext().getSystemService(Context.AUDIO_SERVICE); + return audioManager.getStreamVolume(stream); + } + + public static void adjustStreamVolume (int stream, int volume, int flags) { + final AudioManager audioManager = (AudioManager) UI.getContext().getSystemService(Context.AUDIO_SERVICE); + audioManager.adjustStreamVolume(stream, volume, flags); + } + // ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION opened, but the permission is still not granted. Ignore until the app restarts. public static boolean canReadFile (String url) { diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/MessageView.java b/app/src/main/java/org/thunderdog/challegram/component/chat/MessageView.java index 03fd36d087..74d9816629 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/MessageView.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/MessageView.java @@ -94,6 +94,7 @@ public class MessageView extends SparseDrawableView implements Destroyable, Draw private static final int FLAG_LONG_PRESSED = 1 << 4; private static final int FLAG_DISABLE_MEASURE = 1 << 6; private static final int FLAG_USE_COMPLEX_RECEIVER = 1 << 7; + private static final int FLAG_IGNORE_PARENT_ON_MEASURE = 1 << 8; private @Nullable TGMessage msg; @@ -167,6 +168,10 @@ public void setCustomMeasureDisabled (boolean disabled) { this.flags = BitwiseUtils.setFlag(this.flags, FLAG_DISABLE_MEASURE, disabled); } + public void setParentOnMeasureDisabled (boolean disabled) { + this.flags = BitwiseUtils.setFlag(this.flags, FLAG_IGNORE_PARENT_ON_MEASURE, disabled); + } + @Override public void performDestroy () { avatarReceiver.destroy(); @@ -383,7 +388,9 @@ protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { if ((flags & FLAG_DISABLE_MEASURE) != 0) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } else { - int width = ((View) getParent()).getMeasuredWidth(); + int width = BitwiseUtils.hasFlag(flags, FLAG_IGNORE_PARENT_ON_MEASURE) ? + MeasureSpec.getSize(widthMeasureSpec) : + ((View) getParent()).getMeasuredWidth(); if (msg != null) { msg.buildLayout(width); } diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/MessageViewGroup.java b/app/src/main/java/org/thunderdog/challegram/component/chat/MessageViewGroup.java index be3daa4224..b727cfc3d2 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/MessageViewGroup.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/MessageViewGroup.java @@ -30,6 +30,7 @@ import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; +import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.data.TGMessage; import org.thunderdog.challegram.loader.gif.GifReceiver; import org.thunderdog.challegram.navigation.ViewController; @@ -271,6 +272,7 @@ public void detach () { public void setMessage (TGMessage message) { messageView.setMessage(message); overlayView.setMessage(message); + videoPlayerView.setVisibility(TD.isSelfDestructTypeImmediately(message.getMessage()) ? GONE: VISIBLE); requestVideo(message); if (getMeasuredHeight() != messageView.getCurrentHeight()) { diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesManager.java b/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesManager.java index 82f7f444e3..0d64518bf5 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesManager.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/MessagesManager.java @@ -1065,7 +1065,7 @@ public boolean wouldReusePlayList (TdApi.Message fromMessage, boolean isReverse, } private boolean filterMedia (TdApi.Message message, int contentType) { - return !tdlib.messageSending(message) && contentType == message.content.getConstructor(); + return !TD.isSelfDestructTypeImmediately(message) && !tdlib.messageSending(message) && contentType == message.content.getConstructor(); } @Override diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/VoiceInputView.java b/app/src/main/java/org/thunderdog/challegram/component/chat/VoiceInputView.java deleted file mode 100644 index d36593a777..0000000000 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/VoiceInputView.java +++ /dev/null @@ -1,438 +0,0 @@ -/* - * This file is a part of Telegram X - * Copyright © 2014 (tgx-android@pm.me) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * File created on 04/03/2016 at 13:50 - */ -package org.thunderdog.challegram.component.chat; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.view.Gravity; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.AnticipateOvershootInterpolator; -import android.widget.ImageView; -import android.widget.RelativeLayout; - -import org.thunderdog.challegram.N; -import org.thunderdog.challegram.R; -import org.thunderdog.challegram.core.Background; -import org.thunderdog.challegram.core.Lang; -import org.thunderdog.challegram.data.TGRecord; -import org.thunderdog.challegram.helper.Recorder; -import org.thunderdog.challegram.navigation.ViewController; -import org.thunderdog.challegram.support.RippleSupport; -import org.thunderdog.challegram.support.ViewSupport; -import org.thunderdog.challegram.telegram.TGLegacyAudioManager; -import org.thunderdog.challegram.theme.ColorId; -import org.thunderdog.challegram.theme.Theme; -import org.thunderdog.challegram.tool.Fonts; -import org.thunderdog.challegram.tool.Screen; -import org.thunderdog.challegram.tool.Strings; -import org.thunderdog.challegram.tool.UI; -import org.thunderdog.challegram.tool.Views; - -import me.vkryl.android.AnimatorUtils; -import me.vkryl.android.animator.FactorAnimator; -import me.vkryl.android.util.ClickHelper; -import me.vkryl.android.widget.FrameLayoutFix; - -public class VoiceInputView extends FrameLayoutFix implements View.OnClickListener, TGLegacyAudioManager.PlayListener, ClickHelper.Delegate, FactorAnimator.Target { - private static final long VOICE_START_DELAY = 500l; - private static final long ANIMATION_START_DELAY = 80l; - - public interface Callback { - void onDiscardVoiceRecord (); - } - - private Paint textPaint; - private int textOffset, textRight; - private int waveLeft; - - private Callback callback; - private Waveform waveform; - private ImageView iconView; - - private ClickHelper clickHelper; - - public VoiceInputView (Context context) { - super(context); - - textPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); - textPaint.setTypeface(Fonts.getRobotoRegular()); - textPaint.setTextSize(Screen.dp(15f)); - textOffset = Screen.dp(5f); - textRight = Screen.dp(39f); - waveLeft = Screen.dp(66f); - - this.clickHelper = new ClickHelper(this); - - this.waveform = new Waveform(null, Waveform.MODE_RECT, false); - - iconView = new ImageView(context); - iconView.setId(R.id.btn_discard_record); - iconView.setScaleType(ImageView.ScaleType.CENTER); - iconView.setImageResource(R.drawable.baseline_delete_24); - iconView.setColorFilter(Theme.iconColor()); - iconView.setOnClickListener(this); - iconView.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(58f), ViewGroup.LayoutParams.MATCH_PARENT, Lang.rtl() ? Gravity.RIGHT : Gravity.LEFT)); - Views.setClickable(iconView); - RippleSupport.setTransparentSelector(iconView); - - addView(iconView); - - RelativeLayout.LayoutParams params; - - params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, Screen.dp(48f)); - params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); - if (Lang.rtl()) { - params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); - params.leftMargin = Screen.dp(55f); - } else { - params.addRule(RelativeLayout.ALIGN_PARENT_LEFT); - params.rightMargin = Screen.dp(55f); - } - - setWillNotDraw(false); - setLayoutParams(params); - } - - public void addThemeListeners (ViewController c) { - c.addThemeFilterListener(iconView, ColorId.icon); - c.addThemeInvalidateListener(this); - ViewSupport.setThemedBackground(this, ColorId.filling); - } - - private int calculateWaveformWidth () { - return getMeasuredWidth() - waveLeft - Screen.dp(110f) + Screen.dp(55f); - } - - @Override - protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - if (getMeasuredWidth() != 0) { - waveform.layout(calculateWaveformWidth()); - } - } - - public void setCallback (Callback callback) { - this.callback = callback; - } - - @Override - public void onClick (View v) { - if (v.getId() == R.id.btn_discard_record) { - if (callback != null) { - callback.onDiscardVoiceRecord(); - } - } - } - - @Override - public void onPlayPause (int fileId, boolean isPlaying, boolean isUpdate) { - if (record != null && record.getFileId() == fileId && !isPlaying && !isCaught && !ignoreStop) { - progress = 1f; - updateProgress(); - invalidate(); - } - } - - @Override - public boolean needPlayProgress (int fileId) { - return true; - } - - @Override - public void onPlayProgress (int fileId, float progress, boolean isUpdate) { - if (record != null && record.getFileId() == fileId && progress > 0f) { - this.progress = progress; - if (!updateProgress()) { - invalidate(); - } - } - } - - // Waveform and record stuff - - private boolean ignoreStop; - - public void ignoreStop () { - ignoreStop = true; - } - - public void clearData () { - discardRecord(); - ignoreStop = false; - progress = 0f; - hasStartedPlaying = false; - waveform.setData(null); - invalidate(); - } - - private TGRecord record; - - private void setRecord (final TGRecord record) { - if (this.record != record) { - if (this.record != null) { - TGLegacyAudioManager.instance().unsubscribe(this.record.getAudio().getId(), this); - } - this.record = record; - if (record != null) { - TGLegacyAudioManager.instance().subscribe(record.getAudio().getId(), this); - } - } - } - - public void processRecord (final TGRecord record) { - setRecord(record); - setDuration(record.getDuration()); - Background.instance().post(() -> { - final byte[] waveform = record.getWaveform() != null ? record.getWaveform() : N.getWaveform(record.getPath()); - if (waveform != null) { - UI.post(() -> setWaveform(record, waveform)); - } - }); - } - - public TGRecord getRecord () { - TGRecord record = this.record; - setRecord(null); - return record; - } - - // Waveform animation - - private boolean hasStartedPlaying; - - private final FactorAnimator waveformAnimator = new FactorAnimator(0, this, overshoot, OPEN_DURATION); - - private void setWaveform (final TGRecord record, byte[] waveform) { - if (this.record == null || !this.record.equals(record)) { - return; - } - record.setWaveform(waveform); - this.waveform.setData(waveform); - waveformAnimator.forceFactor(0f); - waveformAnimator.setStartDelay(ANIMATION_START_DELAY); - waveformAnimator.animateTo(1f); - hasStartedPlaying = false; - invalidate(); - } - - @Override - public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { - setExpand(factor); - } - - @Override - public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) { - - } - - private void playPause () { - if (!hasStartedPlaying) { - hasStartedPlaying = true; - updateProgress(); - invalidate(); - playVoice(record); - } else { - /*if (Config.USE_NEW_PLAYER) { - // TODO - } else { - Player.instance().playPause(record.getAudio(), true); - }*/ - } - } - - private void playVoice (TGRecord record) { - if (this.record == null || !this.record.equals(record)) { - return; - } - - /*if (Config.USE_NEW_PLAYER) { - // TODO - } else { - Player.instance().destroy(); - Player.instance().playPause(record.getAudio(), true); - }*/ - } - - public float getExpand () { - return waveform.getExpand(); - } - - public void setExpand (float expand) { - waveform.setExpand(expand); - invalidateWave(); - } - - private void invalidateWave () { - invalidate(waveLeft, 0, waveLeft + waveform.getWidth(), getMeasuredHeight()); - } - - // Duration shit - - private float progress; - private int duration = -1; - private int seek = -1; - private String seekStr; - - public void setDuration (int duration) { - if (this.duration != duration) { - this.duration = duration; - updateProgress(); - } - } - - private boolean updateProgress () { - int seek = (int) ((float) duration * (hasStartedPlaying ? progress : 1f)); - if (this.seek != seek) { - this.seek = seek; - seekStr = Strings.buildDuration(seek); - invalidate(); - return true; - } - return false; - } - - @Override - protected void onDraw (Canvas c) { - int width = getMeasuredWidth(); - int height = getMeasuredHeight(); - int centerY = (int) ((float) height * .5f); - - if (seekStr != null) { - textPaint.setColor(Theme.textAccentColor()); - c.drawText(seekStr, width - textRight, centerY + textOffset, textPaint); - } - - waveform.draw(c, !hasStartedPlaying ? 1f : progress, waveLeft, centerY); - } - - // Record utils - - public void discardRecord () { - if (record != null) { - Recorder.instance().delete(record); - setRecord(null); - } - } - - @Override - public boolean needClickAt (View view, float x, float y) { - return waveform != null && record != null && x >= waveLeft && x < waveLeft + waveform.getWidth(); - } - - @Override - public void onClickAt (View view, float x, float y) { - if (waveform != null && record != null && x >= waveLeft && x < waveLeft + waveform.getWidth()) { - playPause(); - } - } - - // Touch events - - private boolean isCaught; - - @Override - public boolean onTouchEvent (MotionEvent e) { - switch (e.getAction()) { - case MotionEvent.ACTION_DOWN: { - isCaught = false; - - if (Lang.rtl()) { - if (e.getX() > getMeasuredWidth()) { - return false; - } - } else { - if (e.getX() < ((RelativeLayout.LayoutParams) getLayoutParams()).leftMargin) { - return false; - } - } - } - } - - return clickHelper.onTouchEvent(this, e); - } - - // Close Animation - - private static final AnticipateOvershootInterpolator overshoot = new AnticipateOvershootInterpolator(3.0f); - - private float collapse; - - public float getCollapse () { - return collapse; - } - - public void setCollapse (float collapse) { - if (this.collapse != collapse) { - this.collapse = collapse; - if (waveform != null) { - waveform.setExpand(1f - overshoot.getInterpolation(collapse)); - invalidateWave(); - } - } - } - - private ValueAnimator cancel; - private static final long CLOSE_DURATION = 350l; - private static final long FADE_DURATION = 150l; - private static final long OPEN_DURATION = 350l; - - public void animateClose () { - collapse = 0f; - - final long startDelay; - - if (waveform != null && waveform.getMaxSample() != 0) { - final float startFactor = getCollapse(); - final float diffFactor = 1f - startFactor; - cancel = AnimatorUtils.simpleValueAnimator(); - cancel.addUpdateListener(animation -> setCollapse(startFactor + diffFactor * AnimatorUtils.getFraction(animation))); - cancel.setDuration(CLOSE_DURATION); - cancel.setInterpolator(AnimatorUtils.LINEAR_INTERPOLATOR); - cancel.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd (Animator animation) { - cancel = null; - } - }); - startDelay = CLOSE_DURATION - FADE_DURATION; - } else { - cancel = null; - startDelay = 0l; - } - Views.animateAlpha(this, 0f, FADE_DURATION, startDelay, AnimatorUtils.DECELERATE_INTERPOLATOR, new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd (Animator animation) { - setVisibility(View.GONE); - } - }); - if (cancel != null) { - cancel.start(); - } - } - - public void cancelCloseAnimation () { - if (cancel != null) { - cancel.cancel(); - cancel = null; - } - Views.clearAnimations(this); - } -} diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/Waveform.java b/app/src/main/java/org/thunderdog/challegram/component/chat/Waveform.java index 6697322b29..19adf789fe 100644 --- a/app/src/main/java/org/thunderdog/challegram/component/chat/Waveform.java +++ b/app/src/main/java/org/thunderdog/challegram/component/chat/Waveform.java @@ -210,6 +210,10 @@ public void destroy () { } public void draw (Canvas c, float progress, int startX, int centerY) { + draw(c, progress, startX, centerY, false); + } + + public void draw (Canvas c, float progress, int startX, int centerY, boolean hideActive) { switch (mode) { case MODE_BITMAP: { if (chunks == null || bitmap == null || bitmap.isRecycled()) { @@ -226,16 +230,20 @@ public void draw (Canvas c, float progress, int startX, int centerY) { break; } if (progress == 1f) { - int colorId = isOutBubble ? ColorId.bubbleOut_waveformActive : ColorId.waveformActive; - c.drawBitmap(bitmap, startX, topY, PorterDuffPaint.get(colorId)); + if (!hideActive) { + int colorId = isOutBubble ? ColorId.bubbleOut_waveformActive : ColorId.waveformActive; + c.drawBitmap(bitmap, startX, topY, PorterDuffPaint.get(colorId)); + } break; } float endX = progress * (float) currentWidth; - c.save(); - c.clipRect(startX, topY, startX + endX, topY + bitmap.getHeight()); - int colorId = isOutBubble ? ColorId.bubbleOut_waveformActive : ColorId.waveformActive; - c.drawBitmap(bitmap, startX, topY, PorterDuffPaint.get(colorId)); - c.restore(); + if (!hideActive) { + c.save(); + c.clipRect(startX, topY, startX + endX, topY + bitmap.getHeight()); + int colorId = isOutBubble ? ColorId.bubbleOut_waveformActive : ColorId.waveformActive; + c.drawBitmap(bitmap, startX, topY, PorterDuffPaint.get(colorId)); + c.restore(); + } c.save(); c.clipRect(startX + endX, topY, startX + bitmap.getWidth(), topY + bitmap.getHeight()); c.drawBitmap(bitmap, startX, topY, paint); @@ -248,6 +256,9 @@ public void draw (Canvas c, float progress, int startX, int centerY) { } int cx = startX; if (progress == 0f || progress == 1f) { + if (hideActive && progress == 1f ) { + break; + } paint.setColor(Theme.getColor(progress == 0f ? (isOutBubble ? ColorId.bubbleOut_waveformInactive : ColorId.waveformInactive) : (isOutBubble ? ColorId.bubbleOut_waveformActive : ColorId.waveformActive))); for (Chunk chunk : chunks) { chunk.draw(c, cx, centerY, expandFactor, paint); @@ -259,20 +270,22 @@ public void draw (Canvas c, float progress, int startX, int centerY) { int topY = centerY - bound; int bottomY = centerY + bound; float endX = startX + progress * (float) currentWidth; - c.save(); - c.clipRect(startX, topY, endX, bottomY); - paint.setColor(Theme.getColor(isOutBubble ? ColorId.bubbleOut_waveformActive : ColorId.waveformActive)); int i = 0; - for (Chunk chunk : chunks) { - chunk.draw(c, cx, centerY, expandFactor, paint); - cx += width + spacing; - if (cx > endX) { - cx -= width + spacing; - break; + if (!hideActive) { + c.save(); + c.clipRect(startX, topY, endX, bottomY); + paint.setColor(Theme.getColor(isOutBubble ? ColorId.bubbleOut_waveformActive : ColorId.waveformActive)); + for (Chunk chunk : chunks) { + chunk.draw(c, cx, centerY, expandFactor, paint); + cx += width + spacing; + if (cx > endX) { + cx -= width + spacing; + break; + } + i++; } - i++; + c.restore(); } - c.restore(); c.save(); c.clipRect(endX - 1, topY, startX + currentWidth, bottomY); paint.setColor(Theme.getColor(isOutBubble ? ColorId.bubbleOut_waveformInactive : ColorId.waveformInactive)); diff --git a/app/src/main/java/org/thunderdog/challegram/data/FileComponent.java b/app/src/main/java/org/thunderdog/challegram/data/FileComponent.java index 5c96829d96..c572a9a4f0 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/FileComponent.java +++ b/app/src/main/java/org/thunderdog/challegram/data/FileComponent.java @@ -40,6 +40,7 @@ import org.thunderdog.challegram.loader.ImageVideoThumbFile; import org.thunderdog.challegram.loader.Receiver; import org.thunderdog.challegram.mediaview.MediaViewThumbLocation; +import org.thunderdog.challegram.mediaview.disposable.DisposableMediaViewController; import org.thunderdog.challegram.player.TGPlayerController; import org.thunderdog.challegram.telegram.TGLegacyAudioManager; import org.thunderdog.challegram.telegram.Tdlib; @@ -257,7 +258,11 @@ public void setVoice (TdApi.VoiceNote voice, TdApi.Message playPauseFile, TGPlay this.progress.setViewProvider(viewProvider); } - if (context.getChatId() == 0) { // Preview mode + if (TD.isSelfDestructTypeImmediately(message)) { + this.progress.setDownloadedIconRes(R.drawable.baseline_hot_once_24); + this.progress.setIgnorePlayPauseClicks(true); + this.progress.setNoCloud(); + } else if (context.getChatId() == 0) { // Preview mode this.progress.setCurrentState(TdlibFilesManager.STATE_DOWNLOADED_OR_UPLOADED, false); this.progress.setDownloadedIconRes(R.drawable.baseline_pause_24); } @@ -747,7 +752,7 @@ public void draw (T view, Canvas c, int star } int waveformLeft = startX + Screen.dp(FileProgressComponent.DEFAULT_FILE_RADIUS) * 2 + getPreviewOffset(); int cy = startY + Screen.dp(FileProgressComponent.DEFAULT_FILE_RADIUS); - waveform.draw(c, seek, waveformLeft, cy); + waveform.draw(c, seek, waveformLeft, cy, isPlaying && TD.isSelfDestructTypeImmediately(message)); boolean align = context.isOutgoingBubble(); if (unreadFactor != 0f) { int cx = startX + Screen.dp(FileProgressComponent.DEFAULT_FILE_RADIUS); @@ -792,6 +797,15 @@ public void open () { } } + @Override + public boolean onPlayPauseClick (FileProgressComponent context, View view, TdApi.File file, long messageId) { + if (TD.isSelfDestructTypeImmediately(message)) { + return DisposableMediaViewController.openMediaOrShowTooltip(view, this.context, (targetView, outRect) -> progress.toRect(outRect)); + } + + return false; + } + @Override public boolean onClick (FileProgressComponent context, View view, TdApi.File file, long messageId) { if (doc != null) { diff --git a/app/src/main/java/org/thunderdog/challegram/data/TD.java b/app/src/main/java/org/thunderdog/challegram/data/TD.java index b11fdd2959..a11087a474 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TD.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TD.java @@ -5480,6 +5480,10 @@ public static void updateExcludedChatTypes (TdApi.ChatFolder chatFolder, Filter< chatFolder.excludeArchived = filter.accept(R.id.chatType_archived); } + public static boolean isSelfDestructTypeImmediately (TdApi.Message message) { + return message != null && message.selfDestructType != null && message.selfDestructType.getConstructor() == TdApi.MessageSelfDestructTypeImmediately.CONSTRUCTOR; + } + public static final int[] CHAT_TYPES = { R.id.chatType_contact, R.id.chatType_nonContact, diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGAudio.java b/app/src/main/java/org/thunderdog/challegram/data/TGAudio.java index 064d24f3fa..241c99d9ca 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGAudio.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGAudio.java @@ -55,12 +55,6 @@ public TGAudio (Tdlib tdlib, TdApi.Message msg, TdApi.Audio audio) { this.audio = audio; } - public TGAudio (Tdlib tdlib, TGRecord record) { - this.tdlib = tdlib; - this.msg = null; - this.voice = new TdApi.VoiceNote(record.getDuration(), null, "audio/ogg", null, TD.newFile(record.getFile())); - } - public Tdlib tdlib () { return tdlib; } diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGMessage.java b/app/src/main/java/org/thunderdog/challegram/data/TGMessage.java index c9b9245e4d..62a1d4872a 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGMessage.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGMessage.java @@ -2372,7 +2372,7 @@ public final void draw (MessageView view, Canvas c, @NonNull AvatarReceiver avat RectF rectF = Paints.getRectF(); rectF.set(lineLeft, lineTop, lineRight, lineBottom); - final int lineColor = APPLY_ACCENT_TO_FORWARDS && !isOutgoingBubble() && fAuthorNameAccentColor != null ? fAuthorNameAccentColor.getVerticalLineColor() : getVerticalLineColor(); + final int lineColor = getForwardLineColor(); c.drawRoundRect(rectF, lineWidth / 2f, lineWidth / 2f, Paints.fillingPaint(lineColor)); if (mergeTop) { @@ -3532,7 +3532,11 @@ private void buildForward () { } } - private int getForwardAuthorNameLeft () { + public int getForwardLineColor () { + return APPLY_ACCENT_TO_FORWARDS && !isOutgoingBubble() && fAuthorNameAccentColor != null ? fAuthorNameAccentColor.getVerticalLineColor() : getVerticalLineColor(); + } + + public int getForwardAuthorNameLeft () { return useBubbles() ? getInternalBubbleStartX() + Screen.dp(11f) : xfContentLeft; } diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGMessageFile.java b/app/src/main/java/org/thunderdog/challegram/data/TGMessageFile.java index 804080d4ad..7de2945fc7 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGMessageFile.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGMessageFile.java @@ -375,6 +375,12 @@ private CaptionedFile findCaptionedFile (long messageId) { return null; } + @Nullable + public FileComponent findFileComponent (long messageId) { + CaptionedFile file = findCaptionedFile(messageId); + return file != null ? file.component : null; + } + private static final int FLAG_CHANGED_LAYOUT = 1; private static final int FLAG_CHANGED_TEXT_RECEIVERS = 1 << 1; private static final int FLAG_CHANGED_CONTENT_RECEIVERS = 1 << 2; diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGMessageVideo.java b/app/src/main/java/org/thunderdog/challegram/data/TGMessageVideo.java index 4e3985ce85..5508a00b1f 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGMessageVideo.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGMessageVideo.java @@ -16,6 +16,7 @@ import android.graphics.Canvas; import android.graphics.Paint; +import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.view.MotionEvent; @@ -39,6 +40,7 @@ import org.thunderdog.challegram.loader.Receiver; import org.thunderdog.challegram.loader.gif.GifFile; import org.thunderdog.challegram.loader.gif.GifReceiver; +import org.thunderdog.challegram.mediaview.disposable.DisposableMediaViewController; import org.thunderdog.challegram.navigation.ViewController; import org.thunderdog.challegram.player.RoundVideoController; import org.thunderdog.challegram.player.TGPlayerController; @@ -98,6 +100,10 @@ private void setVideoNote (TdApi.VideoNote videoNote) { this.fileProgress.setSimpleListener(this); this.fileProgress.setViewProvider(overlayViews); this.fileProgress.setFile(videoNote.video, getMessage()); + if (TD.isSelfDestructTypeImmediately(getMessage())) { + this.fileProgress.setIgnorePlayPauseClicks(true); + this.fileProgress.setDownloadedIconRes(R.drawable.baseline_hot_once_24); + } if (videoNote.minithumbnail != null) { this.miniThumbnail = new ImageFileLocal(videoNote.minithumbnail); @@ -155,6 +161,16 @@ private void setTrackListenerAttached (boolean attach) { @Override protected boolean updateMessageContent (TdApi.Message message, TdApi.MessageContent newContent, boolean isBottomMessage) { setNotViewed(!((TdApi.MessageVideoNote) newContent).isViewed, true); + if (TD.isSelfDestructTypeImmediately(message)) { + if (newContent != null && newContent.getConstructor() == TdApi.MessageVideoNote.CONSTRUCTOR) { + final var videoNote = ((TdApi.MessageVideoNote) newContent).videoNote; + if (videoNote.minithumbnail != null) { + this.miniThumbnail = new ImageFileLocal(videoNote.minithumbnail); + invalidatePreviewReceiver(); + } + } + } + return false; } @@ -231,6 +247,16 @@ protected final void onChildFactorChanged (int id, float factor, float fraction) } } + @Override + public int getForwardLineColor () { + return ColorUtils.alphaColor(1f - isFullSizeAnimator.getFloatValue(), super.getForwardLineColor()); + } + + @Override + public int getForwardAuthorNameLeft () { + return (int) (super.getForwardAuthorNameLeft() - Screen.dp(11f) * isFullSizeAnimator.getFloatValue()); + } + public void setFullSizeAnimatorDuration (long fullSizeAnimatorDuration) { this.fullSizeAnimatorDuration = fullSizeAnimatorDuration; } @@ -249,11 +275,20 @@ public void autoDownloadContent (TdApi.ChatType type) { fileProgress.downloadAutomatically(type); } + @Override + public boolean onPlayPauseClick (FileProgressComponent context, View view, TdApi.File file, long messageId) { + return onClick(context, view, file, messageId); + } + @Override public boolean onClick (FileProgressComponent context, View view, TdApi.File file, long messageId) { if (Config.ROUND_VIDEOS_PLAYBACK_SUPPORTED) { if (view.getParent() instanceof MessageViewGroup) { - tdlib.context().player().playPauseMessage(tdlib, msg, manager); + if (TD.isSelfDestructTypeImmediately(getMessage())) { + DisposableMediaViewController.openMediaOrShowTooltip(view, this, (targetView, outRect) -> fileProgress.toRect(outRect)); + } else { + tdlib.context().player().playPauseMessage(tdlib, msg, manager); + } } } else { U.openFile(manager.controller(), "video.mp4", new File(file.local.path), "video/mp4", 0); @@ -487,7 +522,27 @@ protected void drawContent (MessageView view, Canvas c, int startX, int startY, if (preview.needPlaceholder()) { preview.drawPlaceholderRounded(c, videoSize / 2f); } - preview.draw(c); + + final boolean drawSpoiler = TD.isSelfDestructTypeImmediately(getMessage()); + final float radius = videoSize / 2f; + final float cx = preview.centerX(), cy = preview.centerY(); + + if (drawSpoiler) { + Receiver spoilerReceiver; + if (preview instanceof DoubleImageReceiver) { + spoilerReceiver = ((DoubleImageReceiver) preview).getPreview(); + } else { + spoilerReceiver = preview; + } + if (spoilerReceiver.isEmpty()) { + spoilerReceiver = preview; + } + spoilerReceiver.draw(c); + + c.drawCircle(cx, cy, radius, Paints.fillingPaint(Theme.getColor(ColorId.spoilerMediaOverlay))); + } else { + preview.draw(c); + } } @Override @@ -523,8 +578,9 @@ protected void drawOverlay (MessageView view, Canvas c, int startX, int startY, c.drawCircle(circleX, textY + Screen.dp(11.5f), Screen.dp(1.5f), Paints.fillingPaint(ColorUtils.alphaColor(viewFactor, useBubbles ? 0xffffffff : Theme.getColor(ColorId.online)))); } + final boolean drawSpoiler = TD.isSelfDestructTypeImmediately(getMessage()); float alpha = (1f - unmuteFactor) * (1f - fileProgress.getBackgroundAlpha()); - if (alpha > 0f) { + if (alpha > 0f && !drawSpoiler) { int radius = Screen.dp(12f); int centerY = receiver.getBottom() - radius - Screen.dp(10f); diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGRecord.java b/app/src/main/java/org/thunderdog/challegram/data/TGRecord.java index aaa3ba9e37..c687881809 100644 --- a/app/src/main/java/org/thunderdog/challegram/data/TGRecord.java +++ b/app/src/main/java/org/thunderdog/challegram/data/TGRecord.java @@ -17,20 +17,14 @@ import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.telegram.Tdlib; -import java.io.File; - public class TGRecord { private final Tdlib.Generation generation; - private File file; private final int duration; private byte[] waveform; - private final TGAudio audio; public TGRecord (Tdlib tdlib, Tdlib.Generation generation, int duration, byte[] waveform) { this.generation = generation; - this.file = new File(generation.destinationPath); this.duration = duration; - this.audio = new TGAudio(tdlib, this); this.waveform = waveform; } @@ -43,21 +37,16 @@ public int getDuration () { return duration; } - public File getFile () { - return file; - } - public String getPath () { - return file.getPath(); - } + if (generation.file.local != null && generation.file.local.isDownloadingCompleted) { + return generation.file.local.path; + } - public TGAudio getAudio () { - return audio; + return generation.destinationPath; } public void setWaveform (byte[] waveform) { this.waveform = waveform; - this.audio.setWaveform(waveform); } public byte[] getWaveform () { @@ -71,10 +60,4 @@ public int getFileId () { public TdApi.InputFile toInputFile () { return new TdApi.InputFileId(generation.file.id); } - - public void delete () { - if (file != null && file.delete()) { - file = null; - } - } } diff --git a/app/src/main/java/org/thunderdog/challegram/filegen/VideoGen.java b/app/src/main/java/org/thunderdog/challegram/filegen/VideoGen.java index 33985e9a19..f35cca1b1f 100644 --- a/app/src/main/java/org/thunderdog/challegram/filegen/VideoGen.java +++ b/app/src/main/java/org/thunderdog/challegram/filegen/VideoGen.java @@ -40,6 +40,13 @@ import androidx.media3.transformer.Transformer; import androidx.media3.transformer.VideoEncoderSettings; +import com.googlecode.mp4parser.BasicContainer; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder; +import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator; +import com.googlecode.mp4parser.authoring.tracks.AppendTrack; +import com.googlecode.mp4parser.authoring.tracks.CroppedTrack; import com.otaliastudios.transcoder.Transcoder; import com.otaliastudios.transcoder.TranscoderListener; import com.otaliastudios.transcoder.common.TrackType; @@ -71,9 +78,11 @@ import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -661,4 +670,79 @@ private boolean convertVideoSimple (String sourcePath, String destinationPath, V videoData.editMovie(destinationPath, info.needMute(), info.getRotate(), (double) info.getStartTimeUs() / 1_000_000.0, info.getEndTimeUs() == -1 ? -1 : (double) info.getEndTimeUs() / 1_000_000.0, onProgress, isCancelled) : videoData.editMovie(destinationPath, info.needMute(), info.getRotate(), onProgress, isCancelled); } + + public static void appendTwoVideos(String firstVideoPath, String secondVideoPath, String output, boolean needTrimFirstVideo, double startTime, double endTime) { + try { + Movie[] inMovies = new Movie[2]; + + inMovies[0] = MovieCreator.build(firstVideoPath); + inMovies[1] = MovieCreator.build(secondVideoPath); + + List videoTracks = new LinkedList<>(); + List audioTracks = new LinkedList<>(); + + for (int a = 0; a < 2; a++) { + final Movie m = inMovies[a]; + for (Track track : m.getTracks()) { + final Track outputTrack; + if (needTrimFirstVideo && a == 0 && startTime != -1 && endTime != -1) { + long currentSample = 0; + double currentTime = 0; + double lastTime = -1; + long startSample = -1; + long endSample = -1; + long timescale = track.getTrackMetaData().getTimescale(); + for (long delta : track.getSampleDurations()) { + if (currentTime > lastTime && currentTime <= startTime) { + // current sample is still before the new starttime + startSample = currentSample; + } + if (currentTime > lastTime && currentTime <= endTime) { + // current sample is after the new start time and still before the new endtime + endSample = currentSample; + } + lastTime = currentTime; + currentTime += (double) delta / (double) timescale; + currentSample++; + } + if (startSample != -1 && endSample == -1) { + endSample = startSample + 1; + } + if (startSample == -1 || endSample == -1) + throw new IllegalArgumentException(); + outputTrack = new CroppedTrack(track, startSample, endSample); + } else { + outputTrack =track; + } + + if (track.getHandler().equals("soun")) { + audioTracks.add(outputTrack); + } + if (track.getHandler().equals("vide")) { + videoTracks.add(outputTrack); + } + } + } + + Movie result = new Movie(); + if (!audioTracks.isEmpty()) { + result.addTrack(new AppendTrack(audioTracks.toArray(new Track[audioTracks.size()]))); + } + if (!videoTracks.isEmpty()) { + result.addTrack(new AppendTrack(videoTracks.toArray(new Track[videoTracks.size()]))); + } + + BasicContainer out = (BasicContainer) new DefaultMp4Builder().build(result); + + FileChannel fc = new RandomAccessFile(output, "rw").getChannel(); + out.writeContainer(fc); + fc.close(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (Throwable e) { + e.printStackTrace(); + } + } } diff --git a/app/src/main/java/org/thunderdog/challegram/helper/Recorder.java b/app/src/main/java/org/thunderdog/challegram/helper/Recorder.java index 15902d6119..76ba5eb799 100644 --- a/app/src/main/java/org/thunderdog/challegram/helper/Recorder.java +++ b/app/src/main/java/org/thunderdog/challegram/helper/Recorder.java @@ -25,8 +25,8 @@ import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.Log; import org.thunderdog.challegram.N; +import org.thunderdog.challegram.U; import org.thunderdog.challegram.core.BaseThread; -import org.thunderdog.challegram.data.TGRecord; import org.thunderdog.challegram.filegen.GenerationInfo; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.tool.UI; @@ -55,8 +55,7 @@ public static Recorder instance () { private static BaseThread recordThread, encodeThread; private Tdlib.Generation currentGeneration; - private Tdlib.Generation generationToRemove; - private boolean isRecording; + private boolean isRecording, isFinished; private long samplesCount; private short[] recordSamples = new short[1024]; @@ -76,10 +75,17 @@ private Recorder () { private void setRecording (final boolean isRecording) { synchronized (this) { this.isRecording = isRecording; + if (isRecording) { + this.isFinished = false; + } } } public void record (final Tdlib tdlib, final boolean isSecret, final Listener listener) { + record(tdlib, isSecret, listener, false); + } + + private void record (final Tdlib tdlib, final boolean isSecret, final Listener listener, boolean isResume) { setRecording(true); encodeThread.post(() -> recordThread.post(startRunnable = new CancellableRunnable() { @Override @@ -90,27 +96,42 @@ public void act () { return; } } - startRecording(tdlib, isSecret, listener); + startRecording(tdlib, isSecret, listener, isResume); } }, START_DELAY), 0); } - public void save () { + public void resume (final Tdlib tdlib, final boolean isSecret, final Listener listener) { + record(tdlib, isSecret, listener, true); + } + + public void save (boolean allowResuming) { setRecording(false); - if (SystemClock.elapsedRealtime() - recordStart < 700l) { + if ((allowResuming ? recordTimeCount : 0) + (SystemClock.elapsedRealtime() - recordStart) < 700l) { cancel(); return; } - stopRecording(false); + stopRecording(false, allowResuming); } public void cancel () { setRecording(false); - stopRecording(true); + stopRecording(true, false); } - public void delete (final TGRecord record) { - recordThread.post(() -> record.delete(), 0); + public void finish (boolean isCanceled) { + synchronized (this) { + isFinished = true; + } + + encodeThread.post(() -> { + if (recorder == null) { + if (currentGeneration != null) { + tdlib.finishGeneration(currentGeneration, isCanceled ? new TdApi.Error(-1, "Canceled") : null); + currentGeneration = null; + } + } + }, 0); } // Internal @@ -120,7 +141,7 @@ private void dispatchError () { tdlib.finishGeneration(currentGeneration, new TdApi.Error()); currentGeneration = null; } - encodeThread.post(() -> cleanupRecording(true), 0); + encodeThread.post(() -> cleanupRecording(true, false), 0); UI.post(() -> listener.onFail()); } @@ -151,12 +172,13 @@ private void dispatchProgress () { private ByteBuffer fileBuffer; private int bufferSize; - private void startRecording (Tdlib tdlib, boolean isSecret, Recorder.Listener listener) { + private void startRecording (Tdlib tdlib, boolean isSecret, Recorder.Listener listener, boolean isResume) { this.tdlib = tdlib; this.listener = listener; final String id = "voice" + GenerationInfo.randomStamp(); - Tdlib.Generation generation = tdlib.generateFile(id, new TdApi.FileTypeVoiceNote(), isSecret, 1, 5000); + Tdlib.Generation generation = isResume ? currentGeneration : + tdlib.generateFile(id, new TdApi.FileTypeVoiceNote(), isSecret, 1, 5000); if (generation == null) { dispatchError(); @@ -165,14 +187,22 @@ private void startRecording (Tdlib tdlib, boolean isSecret, Recorder.Listener li currentGeneration = generation; - if (generationToRemove != null && new File(generationToRemove.destinationPath).delete()) { - generationToRemove = null; - } - try { - if (N.startRecord(generation.destinationPath) == 0) { - dispatchError(); - return; + if (isResume) { + if (resumeFile == null || !U.moveFile(resumeFile, new File(generation.destinationPath + ".resume"))) { + dispatchError(); + return; + } + + if (N.resumeRecord(generation.destinationPath, 48000) == 0) { + dispatchError(); + return; + } + } else { + if (N.startRecord(generation.destinationPath, 48000) == 0) { + dispatchError(); + return; + } } if (bufferSize == 0) { @@ -208,11 +238,16 @@ private void startRecording (Tdlib tdlib, boolean isSecret, Recorder.Listener li try { tryInitEnhancers(); - recordStart = SystemClock.elapsedRealtime(); - recordTimeCount = 0; + if (!isResume) { + recordStart = SystemClock.elapsedRealtime(); + recordTimeCount = 0; + } removeFile = true; + allowResuming = false; recorder.startRecording(); - initMaxAmplitude(); + if (!isResume) { + initMaxAmplitude(); + } dispatchRecord(); } catch (Throwable t) { if (recorder != null) { @@ -225,18 +260,30 @@ private void startRecording (Tdlib tdlib, boolean isSecret, Recorder.Listener li } } - // private File fileToRemove; + private File resumeFile; - private void cleanupRecording (boolean removeFile) { - N.stopRecord(); + private void cleanupRecording (boolean removeFile, boolean allowResuming) { + N.stopRecord(!removeFile && allowResuming); setRecording(false); if (currentGeneration != null) { - tdlib.finishGeneration(currentGeneration, removeFile ? new TdApi.Error(-1, "Canceled") : null); - if (removeFile) { - generationToRemove = currentGeneration; - } else if (listener != null) { + if (!removeFile && allowResuming) { + final File resumeSrc = new File(currentGeneration.destinationPath + ".resume"); + if (resumeFile == null) { + resumeFile = new File(resumeSrc.getParent(), "voice.resume"); + } + U.moveFile(resumeSrc, resumeFile); + } + if (!removeFile && listener != null) { listener.onSave(currentGeneration, Math.round((float) recordTimeCount / 1000f), getWaveform()); } + if (removeFile || isFinished || !allowResuming) { + tdlib.finishGeneration(currentGeneration, removeFile ? new TdApi.Error(-1, "Canceled") : null); + currentGeneration = null; + } + /* else { + long size = new File(currentGeneration.destinationPath).length(); + tdlib.client().send(new TdApi.SetFileGenerationProgress(currentGeneration.generationId, 0, size), tdlib.silentHandler()); + }*/ } if (recorder != null) { recorder.release(); @@ -270,7 +317,7 @@ public void run () { if (length <= 0) { buffers.add(buffer); - encodeThread.post(() -> cleanupRecording(removeFile), 0); + encodeThread.post(() -> cleanupRecording(removeFile, allowResuming), 0); return; } @@ -296,6 +343,7 @@ private void processBuffer (final ByteBuffer buffer, boolean flush) { fileBuffer.put(buffer); if (fileBuffer.position() == fileBuffer.limit() || flush) { if (N.writeFrame(fileBuffer, !flush ? fileBuffer.limit() : buffer.position()) != 0) { + // tdlib.client().send(new TdApi.SetFileGenerationProgress(currentGeneration.generationId, 0, new File(currentGeneration.destinationPath).length()), tdlib.silentHandler()); fileBuffer.rewind(); recordTimeCount += fileBuffer.limit() / 3 / 2 / 16; } @@ -308,8 +356,9 @@ private void processBuffer (final ByteBuffer buffer, boolean flush) { } private boolean removeFile; + private boolean allowResuming; - private void stopRecording (final boolean removeFile) { + private void stopRecording (final boolean removeFile, final boolean allowResuming) { encodeThread.post(() -> { final boolean started; if (startRunnable != null) { @@ -322,6 +371,7 @@ private void stopRecording (final boolean removeFile) { recordThread.post(() -> { if (started) { Recorder.this.removeFile = removeFile; + Recorder.this.allowResuming = allowResuming; if (recorder == null) { return; } @@ -332,7 +382,7 @@ private void stopRecording (final boolean removeFile) { Log.e("Cannot stop recorder", t); } } else { - cleanupRecording(removeFile); + cleanupRecording(removeFile, allowResuming); } }, 0); }, 0); diff --git a/app/src/main/java/org/thunderdog/challegram/mediaview/disposable/DisposableMediaViewController.java b/app/src/main/java/org/thunderdog/challegram/mediaview/disposable/DisposableMediaViewController.java new file mode 100644 index 0000000000..88deefda8e --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/mediaview/disposable/DisposableMediaViewController.java @@ -0,0 +1,349 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 30/06/2024 + */ +package org.thunderdog.challegram.mediaview.disposable; + +import android.content.Context; +import android.media.AudioManager; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.common.Player; +import androidx.media3.exoplayer.ExoPlayer; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.BaseActivity; +import org.thunderdog.challegram.Log; +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.U; +import org.thunderdog.challegram.data.TD; +import org.thunderdog.challegram.data.TGMessage; +import org.thunderdog.challegram.navigation.TooltipOverlayView; +import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.player.TGPlayerController; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.telegram.TdlibManager; +import org.thunderdog.challegram.tool.UI; +import org.thunderdog.challegram.widget.PopupLayout; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.RandomAccessFile; + +import me.vkryl.android.AnimatorUtils; +import me.vkryl.android.animator.FactorAnimator; +import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.MathUtils; +import me.vkryl.td.Td; + +public abstract class DisposableMediaViewController extends ViewController implements + PopupLayout.AnimatedPopupProvider, FactorAnimator.Target, Player.Listener, PopupLayout.TouchSectionProvider { + + public DisposableMediaViewController (@NonNull Context context, Tdlib tdlib) { + super(context, tdlib); + } + + private PopupLayout popupView; + private ExoPlayer exoPlayer; + protected FrameLayoutFix contentView; + + @Override + protected final View onCreateView (Context context) { + popupView = new PopupLayout(context); + popupView.setOverlayStatusBar(true); + popupView.setTouchProvider(this); + popupView.setNeedRootInsets(); + popupView.init(true); + popupView.setIgnoreAllInsets(true); + popupView.setBoundController(this); + popupView.setDisableCancelOnTouchDown(true); + + contentView = onCreateContentView(context); + + exoPlayer = U.newExoPlayer(context, true); + TdlibManager.instance().player().proximityManager().modifyExoPlayer(exoPlayer, C.AUDIO_CONTENT_TYPE_MOVIE); + exoPlayer.addListener(this); + exoPlayer.setVolume(1f); + exoPlayer.setPlayWhenReady(false); + exoPlayer.setMediaSource(U.newMediaSource(contentFile)); + onExoPlayerCreated(exoPlayer); + exoPlayer.prepare(); + + return contentView; + } + + @Override + public boolean shouldTouchOutside (float x, float y) { + return false; + } + + @Override + public void onPlaybackStateChanged (@Player.State int playbackState) { + if (playbackState == Player.STATE_ENDED) { + hide(); + } + } + + abstract protected FrameLayoutFix onCreateContentView (Context context); + + abstract protected void onExoPlayerCreated (ExoPlayer exoPlayer); + + public final void open () { + getValue(); + popupView.showAnimatedPopupView(contentView, this); + } + + public final void hide () { + popupView.hideWindow(true); + } + + public final long getExoPlayerDuration () { + return exoPlayer != null ? exoPlayer.getDuration() : C.TIME_UNSET; + } + + public final long getExoPlayerCurrentPosition () { + return exoPlayer != null ? exoPlayer.getCurrentPosition() : C.TIME_UNSET; + } + + @Override + public void prepareShowAnimation () { + revealAnimator = new FactorAnimator(ANIMATOR_REVEAL, this, AnimatorUtils.DECELERATE_INTERPOLATOR, REVEAL_ANIMATION_DURATION); + } + + @Override + public void launchShowAnimation (PopupLayout popup) { + revealAnimator.animateTo(1f); + } + + @Override + public boolean launchHideAnimation (PopupLayout popup, FactorAnimator originalAnimator) { + revealAnimator.animateTo(0f); + return true; + } + + private static final long REVEAL_ANIMATION_DURATION = 280; + private static final int ANIMATOR_REVEAL = -1; + private FactorAnimator revealAnimator; + private float revealFactor; + + private void setRevealFactor (float revealFactor) { + if (this.revealFactor != revealFactor) { + this.revealFactor = revealFactor; + onChangeRevealFactor(revealFactor); + } + } + + protected final float getRevealFactor () { + return revealFactor; + } + + abstract protected void onChangeRevealFactor (float factor); + + @Override + public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { + if (id == ANIMATOR_REVEAL) { + setRevealFactor(factor); + } + } + + @Override + public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) { + if (id == ANIMATOR_REVEAL) { + if (finalFactor == 0f) { + popupView.onCustomHideAnimationComplete(); + } + if (finalFactor == 1f) { + tgMessage.readContent(); + popupView.onCustomShowComplete(); + exoPlayer.setPlayWhenReady(true); + checkProgressTimer(true); + } + } + } + + + /* Args */ + + protected TGMessage tgMessage; + protected View anchorView; + private RandomAccessFile contentFile; + + protected static class Args { + private final TGMessage message; + private final View anchorView; + private final RandomAccessFile contentFile; + + public Args(@Nullable View view, @NonNull TGMessage message, @NonNull RandomAccessFile file) { + this.message = message; + this.anchorView = view; + this.contentFile = file; + } + } + + @Override + public void setArguments (Args args) { + super.setArguments(args); + this.tgMessage = args.message; + this.anchorView = args.anchorView; + this.contentFile = args.contentFile; + } + + @Override + public void destroy () { + super.destroy(); + + checkProgressTimer(false); + + exoPlayer.release(); + exoPlayer = null; + + U.closeFile(contentFile); + } + + + public static boolean openMediaOrShowTooltip (View view, TGMessage message, TooltipOverlayView.LocationProvider locationProvider) { + final BaseActivity context = message.context(); + final Tdlib tdlib = message.tdlib(); + final TdApi.Message msg = message.getMessage(); + final TdApi.File file = TD.getFile(msg); + + if (!TD.isFileLoaded(file)) { + UI.hapticVibrate(view, true); + context.tooltipManager().builder(view).locate(locationProvider).show(tdlib, R.string.MediaOnceWaitFilуDownload).hideDelayed(); + return false; + } + + if (U.getStreamVolume(AudioManager.STREAM_MUSIC) == 0) { + U.adjustStreamVolume(AudioManager.STREAM_MUSIC, 0, AudioManager.FLAG_SHOW_UI); + UI.hapticVibrate(view, true); + context.tooltipManager().builder(view).locate(locationProvider).show(tdlib, R.string.MediaOnceTurnOnSound).hideDelayed(); + return false; + } + + DisposableMediaViewController player = null; + if (Td.isVideoNote(msg.content)) { + player = new DisposableMediaViewControllerVideo(context, tdlib); + } else if (Td.isVoiceNote(msg.content)) { + player = new DisposableMediaViewControllerAudio(context, tdlib); + } + + if (player != null) { + RandomAccessFile randomAccessFile; + try { + randomAccessFile = new RandomAccessFile(new File(file.local.path), "r"); + } catch (FileNotFoundException e) { + UI.hapticVibrate(view, true); + Log.e(e); + return false; + } + + TdlibManager.instance().player().pauseWithReason(TGPlayerController.PAUSE_REASON_OPEN_ONCE_MEDIA); + + player.setArguments(new Args(view, message, randomAccessFile)); + player.open(); + return true; + } + + return false; + } + + + + + /* Visual Progress */ + + private static final int ACTION_PROGRESS_TICK = 1; + + private final VideoHandler handler = new VideoHandler(this); + + private static class VideoHandler extends Handler { + private final DisposableMediaViewController controller; + + public VideoHandler (DisposableMediaViewController controller) { + super(Looper.getMainLooper()); + this.controller = controller; + } + + @Override + public void handleMessage (@NonNull Message msg) { + controller.onProgressTick(); + } + } + + private boolean progressTimerStarted; + + private void checkProgressTimer (boolean isPlayed) { + if (this.progressTimerStarted != isPlayed) { + this.progressTimerStarted = isPlayed; + Log.i(Log.TAG_VIDEO, "progressTimerStarted -> %b", isPlayed); + handler.removeMessages(ACTION_PROGRESS_TICK); + onProgressTick(); + } + } + + private float progress; + private long playPosition = -1; + private long playDuration = -1; + + private void setPlayProgress (float progress, long playPosition, long playDuration) { + if (this.progress != progress || this.playPosition != playPosition || this.playDuration != playDuration) { + // boolean reset = this.remainingSeconds != remainingSeconds || this.totalSeconds != totalSeconds; + + this.progress = progress; + this.playDuration = playDuration; + if (this.playPosition != playPosition) { + this.playPosition = playPosition; + } + + setVisualProgress(MathUtils.clamp(progress)); + } + } + + private float visualProgress; + + public float getVisualProgress () { + return visualProgress; + } + + private void setVisualProgress (float progress) { + if (this.visualProgress != progress) { + this.visualProgress = progress; + onChangeVisualProgress(progress); + } + } + + private void onProgressTick () { + final long duration = getExoPlayerDuration(); + final long position = getExoPlayerCurrentPosition(); + if (duration != C.TIME_UNSET && position != C.TIME_UNSET) { + float progress = duration != 0 ? MathUtils.clamp((float) position / (float) duration) : 0f; + setPlayProgress(progress, position, duration); + } + + if (progressTimerStarted) { + long delay = calculateProgressTickDelay(playDuration); + handler.sendMessageDelayed(Message.obtain(handler, ACTION_PROGRESS_TICK), delay); + } + } + + protected abstract void onChangeVisualProgress (float progress); + + protected abstract long calculateProgressTickDelay (long playDuration); + +} diff --git a/app/src/main/java/org/thunderdog/challegram/mediaview/disposable/DisposableMediaViewControllerAudio.java b/app/src/main/java/org/thunderdog/challegram/mediaview/disposable/DisposableMediaViewControllerAudio.java new file mode 100644 index 0000000000..6a9be78036 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/mediaview/disposable/DisposableMediaViewControllerAudio.java @@ -0,0 +1,201 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 30/06/2024 + */ +package org.thunderdog.challegram.mediaview.disposable; + +import android.content.Context; +import android.graphics.Color; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.media3.exoplayer.ExoPlayer; + +import org.drinkless.tdlib.TdApi; +import org.thunderdog.challegram.U; +import org.thunderdog.challegram.component.chat.MessageView; +import org.thunderdog.challegram.data.TD; +import org.thunderdog.challegram.data.TGMessage; +import org.thunderdog.challegram.data.TGMessageFile; +import org.thunderdog.challegram.player.TGPlayerController; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.widget.FileProgressComponent; +import org.thunderdog.challegram.widget.PopupLayout; + +import me.vkryl.android.animator.FactorAnimator; +import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.ColorUtils; +import me.vkryl.core.MathUtils; +import me.vkryl.td.Td; + +class DisposableMediaViewControllerAudio extends DisposableMediaViewController { + public DisposableMediaViewControllerAudio(@NonNull Context context, Tdlib tdlib) { + super(context, tdlib); + } + + private MessageView messageView; + + @Override + protected FrameLayoutFix onCreateContentView (Context context) { + contentView = new ContentView(context); + + messageView = new MessageView(context) { + @Override + public boolean onTouchEvent (MotionEvent e) { + return false; + } + + @Override + protected void onAttachedToWindow () { + super.onAttachedToWindow(); + onAttachedToRecyclerView(); + } + + @Override + protected void onDetachedFromWindow () { + super.onDetachedFromWindow(); + onDetachedFromRecyclerView(); + } + }; + messageView.setParentOnMeasureDisabled(true); + messageView.setUseComplexReceiver(); + messageView.setManager(copy.manager()); + messageView.setMessage(copy); + contentView.addView(messageView); + + return contentView; + } + + @Override + protected void onExoPlayerCreated (ExoPlayer exoPlayer) { + + } + + private float revealStartPosition; + private float revealEndPosition; + + private static final int[] tmpCords = new int[2]; + + @Override + public void prepareShowAnimation () { + if (anchorView != null) { + anchorView.getLocationOnScreen(tmpCords); + revealStartPosition = tmpCords[1]; + } else { + revealStartPosition = 0; + } + + onChangeRevealFactor(getRevealFactor()); + super.prepareShowAnimation(); + } + + @Override + public boolean launchHideAnimation (PopupLayout popup, FactorAnimator originalAnimator) { + revealStartPosition = revealEndPosition; + return super.launchHideAnimation(popup, originalAnimator); + } + + @Override + protected void onChangeRevealFactor (float factor) { + final int viewWidth = contentView.getMeasuredWidth(); + final int viewHeight = contentView.getMeasuredHeight(); + final int messageHeight = messageView.getMeasuredHeight(); + if (viewWidth == 0 || viewHeight == 0 || messageHeight == 0) { + return; + } + + revealEndPosition = (viewHeight - messageHeight) / 2f; + if (anchorView == null) { + revealStartPosition = revealEndPosition; + } + + messageView.setAlpha(factor); + messageView.setTranslationY(Math.round(MathUtils.fromTo(revealStartPosition, revealEndPosition, factor))); + + context.setPhotoRevealFactor(factor); + contentView.setBackgroundColor(ColorUtils.alphaColor(factor * 0.9f, Color.BLACK)); + contentView.invalidate(); + } + + @Override + protected void onChangeVisualProgress (float progress) { + if (fileProgressComponent != null) { + // send fake progress event + fileProgressComponent.onTrackPlayProgress(tdlib, msgCopy.chatId, msgCopy.id, TD.getFileId(msgCopy), progress, getExoPlayerCurrentPosition(), getExoPlayerDuration(), false); + } + contentView.invalidate(); + } + + @Override + protected long calculateProgressTickDelay (long playDuration) { + return U.calculateDelayForDiameter(getVideoSize(), playDuration); + } + + @Override + public int getId () { + return 0; + } + + @Override + public void destroy () { + messageView.performDestroy(); + super.destroy(); + } + + + + /* Content */ + + public class ContentView extends FrameLayoutFix { + public ContentView (@NonNull Context context) { + super(context); + } + + @Override + protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + onChangeRevealFactor(getRevealFactor()); + } + } + + private static int getVideoSize () { + return Math.min(Screen.smallestSide() - Screen.dp(80), Screen.dp(640)); + } + + + private TGMessageFile copy; + protected TdApi.Message msgCopy; + private FileProgressComponent fileProgressComponent; + + @Override + public void setArguments (Args args) { + super.setArguments(args); + + msgCopy = Td.copyOf(tgMessage.getMessage()); + msgCopy.chatId = -1; // set fake chat id and mark as listened + if (msgCopy.content.getConstructor() == TdApi.MessageVoiceNote.CONSTRUCTOR) { + ((TdApi.MessageVoiceNote) msgCopy.content).isListened = true; + } + + copy = (TGMessageFile) TGMessage.valueOf(tgMessage.manager(), msgCopy); + + final var file = copy.findFileComponent(msgCopy.id); + fileProgressComponent = file != null ? file.getFileProgress() : null; + if (fileProgressComponent != null) { + // send fake play event + fileProgressComponent.onTrackStateChanged(tdlib, msgCopy.chatId, msgCopy.id, TD.getFileId(msgCopy), TGPlayerController.STATE_PLAYING); + tdlib.files().unsubscribe(TD.getFileId(msgCopy), fileProgressComponent); + } + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/mediaview/disposable/DisposableMediaViewControllerVideo.java b/app/src/main/java/org/thunderdog/challegram/mediaview/disposable/DisposableMediaViewControllerVideo.java new file mode 100644 index 0000000000..3edb767b71 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/mediaview/disposable/DisposableMediaViewControllerVideo.java @@ -0,0 +1,257 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 30/06/2024 + */ +package org.thunderdog.challegram.mediaview.disposable; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.view.SurfaceView; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.media3.exoplayer.ExoPlayer; + +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.U; +import org.thunderdog.challegram.loader.DoubleImageReceiver; +import org.thunderdog.challegram.player.RoundVideoController; +import org.thunderdog.challegram.telegram.Tdlib; +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.tool.Drawables; +import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.PorterDuffPaint; +import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.widget.CircleFrameLayout; +import org.thunderdog.challegram.widget.PopupLayout; + +import me.vkryl.android.animator.FactorAnimator; +import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.ColorUtils; +import me.vkryl.core.MathUtils; + +class DisposableMediaViewControllerVideo extends DisposableMediaViewController { + public DisposableMediaViewControllerVideo (@NonNull Context context, Tdlib tdlib) { + super(context, tdlib); + } + + private CircleFrameLayout mainPlayerView; + private View mainTextureView; + + private DoubleImageReceiver previewReceiver; + + @Override + protected FrameLayoutFix onCreateContentView (Context context) { + contentView = new ContentView(context); + + final int textureSize = getVideoSize(); + previewReceiver = new DoubleImageReceiver(contentView, textureSize / 2); + tgMessage.requestPreview(previewReceiver); + + mainPlayerView = new CircleFrameLayout(context); + mainPlayerView.setAlpha(getRevealFactor()); + mainPlayerView.setLayoutParams(FrameLayoutFix.newParams(textureSize, textureSize)); + mainPlayerView.setPivotX(0); + mainPlayerView.setPivotY(0); + + if (RoundVideoController.USE_SURFACE) { + mainTextureView = new SurfaceView(context); + } else { + mainTextureView = new TextureView(context); + } + mainTextureView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + mainPlayerView.addView(mainTextureView); + + contentView.addView(mainPlayerView); + + return contentView; + } + + @Override + protected void onExoPlayerCreated (ExoPlayer exoPlayer) { + if (mainTextureView instanceof SurfaceView) { + exoPlayer.setVideoSurfaceView((SurfaceView) mainTextureView); + } else { + exoPlayer.setVideoTextureView((TextureView) mainTextureView); + } + } + + private RectF revealStartPosition; + private final RectF revealPosition = new RectF(); + private final RectF progressBounds = new RectF(); + private final Rect revealPositionBounds = new Rect(); + private final RectF revealEndPosition = new RectF(); + private float removeDegrees; + + private static final int[] tmpCords = new int[2]; + + @Override + public void prepareShowAnimation () { + if (anchorView != null) { + revealStartPosition = new RectF(); + anchorView.getLocationOnScreen(tmpCords); + + final int left = tmpCords[0] + tgMessage.getContentX(); + final int top = tmpCords[1] + tgMessage.getContentY(); + final int width = tgMessage.getChildrenWidth(); + final int height = tgMessage.getChildrenHeight(); + + revealStartPosition.set(left, top, left + width, top + height); + } else { + revealStartPosition = null; + } + + onChangeRevealFactor(getRevealFactor()); + super.prepareShowAnimation(); + } + + @Override + public boolean launchHideAnimation (PopupLayout popup, FactorAnimator originalAnimator) { + revealStartPosition.set(revealEndPosition); + return super.launchHideAnimation(popup, originalAnimator); + } + + @Override + protected void onChangeRevealFactor (float factor) { + final int viewWidth = contentView.getMeasuredWidth(); + final int viewHeight = contentView.getMeasuredHeight(); + final int videoSize = getVideoSize(); + if (viewWidth == 0 || viewHeight == 0) { + return; + } + + revealEndPosition.set( + (viewWidth - videoSize) / 2f, + (viewHeight - videoSize) / 2f, + (viewWidth + videoSize) / 2f, + (viewHeight + videoSize) / 2f + ); + + if (revealStartPosition == null) { + revealStartPosition = new RectF(revealEndPosition); + } + + revealPosition.set( + MathUtils.fromTo(revealStartPosition.left, revealEndPosition.left, factor), + MathUtils.fromTo(revealStartPosition.top, revealEndPosition.top, factor), + MathUtils.fromTo(revealStartPosition.right, revealEndPosition.right, factor), + MathUtils.fromTo(revealStartPosition.bottom, revealEndPosition.bottom, factor) + ); + revealPosition.round(revealPositionBounds); + previewReceiver.setBounds(revealPositionBounds.left, revealPositionBounds.top, revealPositionBounds.right, revealPositionBounds.bottom); + + progressBounds.set(revealPosition); + progressBounds.inset(-Screen.dp(10), -Screen.dp(10)); + + mainPlayerView.setAlpha(factor); + mainPlayerView.setTranslationX(revealPosition.left); + mainPlayerView.setTranslationY(revealPosition.top); + mainPlayerView.setScaleX(revealPosition.width() / revealEndPosition.width()); + mainPlayerView.setScaleY(revealPosition.height() / revealEndPosition.height()); + + final double removeDistance = Paints.videoStrokePaint().getStrokeWidth(); + final double totalDistance = (int) (2.0 * Math.PI * (double) (progressBounds.width() / 2f)); + this.removeDegrees = (float) (removeDistance / totalDistance) * 360f; + + context.setPhotoRevealFactor(factor); + contentView.setBackgroundColor(ColorUtils.alphaColor(factor * 0.9f, Color.BLACK)); + contentView.invalidate(); + } + + @Override + protected void onChangeVisualProgress (float progress) { + contentView.invalidate(); + } + + @Override + protected long calculateProgressTickDelay (long playDuration) { + return U.calculateDelayForDiameter(getVideoSize(), playDuration); + } + + @Override + public int getId () { + return 0; + } + + @Override + public void destroy () { + previewReceiver.destroy(); + super.destroy(); + } + + + + /* Content */ + + public class ContentView extends FrameLayoutFix { + private static final int STROKE_WIDTH = 4; + private final Paint strokePaint; + private final Drawable drawable; + + public ContentView (@NonNull Context context) { + super(context); + strokePaint = new Paint(Paints.videoStrokePaint()); + drawable = Drawables.get(context.getResources(), R.drawable.baseline_hot_once_24); + } + + @Override + protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + onChangeRevealFactor(getRevealFactor()); + } + + @Override + protected void onAttachedToWindow () { + super.onAttachedToWindow(); + previewReceiver.attach(); + } + + @Override + protected void onDetachedFromWindow () { + super.onDetachedFromWindow(); + previewReceiver.detach(); + } + + @Override + protected void dispatchDraw (@NonNull Canvas canvas) { + final float alpha = getRevealFactor(); + final float decorationsAlpha = alpha * alpha; + + Drawables.drawCentered(canvas, drawable, revealPosition.centerX(), revealPosition.top - Screen.dp(38), PorterDuffPaint.get(ColorId.white, decorationsAlpha)); + + previewReceiver.setAlpha(alpha); + if (previewReceiver.needPlaceholder()) { + previewReceiver.drawPlaceholderRounded(canvas, revealPositionBounds.width() / 2f); + } + previewReceiver.draw(canvas); + + strokePaint.setStrokeWidth(Screen.dp(STROKE_WIDTH)); + strokePaint.setColor(ColorUtils.alphaColor(decorationsAlpha, Color.WHITE)); + + canvas.drawArc(progressBounds, -90, (360f - removeDegrees) * (getVisualProgress() - 1f), false, strokePaint); + + super.dispatchDraw(canvas); + } + } + + private static int getVideoSize () { + return Math.min(Screen.smallestSide() - Screen.dp(80), Screen.dp(640)); + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/player/RecordAudioVideoController.java b/app/src/main/java/org/thunderdog/challegram/player/RecordAudioVideoController.java index 9ebb26156b..490e87450c 100644 --- a/app/src/main/java/org/thunderdog/challegram/player/RecordAudioVideoController.java +++ b/app/src/main/java/org/thunderdog/challegram/player/RecordAudioVideoController.java @@ -32,15 +32,18 @@ import org.drinkless.tdlib.TdApi; import org.thunderdog.challegram.BaseActivity; import org.thunderdog.challegram.Log; +import org.thunderdog.challegram.N; import org.thunderdog.challegram.R; +import org.thunderdog.challegram.U; import org.thunderdog.challegram.component.chat.VoiceVideoButtonView; +import org.thunderdog.challegram.core.Background; import org.thunderdog.challegram.core.Lang; import org.thunderdog.challegram.data.TD; import org.thunderdog.challegram.data.TGRecord; +import org.thunderdog.challegram.filegen.VideoGen; import org.thunderdog.challegram.filegen.VideoGenerationInfo; import org.thunderdog.challegram.helper.Recorder; import org.thunderdog.challegram.navigation.ViewController; -import org.thunderdog.challegram.support.RippleSupport; import org.thunderdog.challegram.support.ViewSupport; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.telegram.TdlibFilesManager; @@ -67,6 +70,7 @@ import org.thunderdog.challegram.widget.SimpleVideoPlayer; import org.thunderdog.challegram.widget.VideoTimelineView; +import java.io.File; import java.lang.ref.Reference; import java.util.ArrayList; import java.util.List; @@ -103,7 +107,8 @@ public class RecordAudioVideoController implements private VoiceVideoButtonView voiceVideoButtonView; private RecordLockView lockView; private CameraControlButton switchCameraButton; - private FrameLayoutFix switchCameraButtonWrap; + private RecordControllerButton switchCameraButtonWrap; + private RecordDisposableSwitchButton disposableSwitchButton; private RecordDurationView durationView; private FrameLayoutFix inputOverlayView; private TextView slideHintView; @@ -111,12 +116,13 @@ public class RecordAudioVideoController implements private View cornerView; private CircleFrameLayout videoLayout; private View videoPlaceholderView; - private RoundProgressView progressView; + private RoundProgressView3 progressView; private SendButton sendButton; private ImageView deleteButton; private HapticMenuHelper sendHelper; private VideoTimelineView videoTimelineView; private SimpleVideoPlayer videoPreviewView; + private VoiceWaveformView audioPreviewView; private ImageView muteIcon; private boolean preferVideoMode; @@ -166,6 +172,7 @@ private void updateColors () { this.cornerView.invalidate(); this.switchCameraButton.invalidate(); this.switchCameraButtonWrap.invalidate(); + this.disposableSwitchButton.invalidate(); this.videoTimelineView.invalidate(); this.durationView.invalidate(); this.lockView.invalidate(); @@ -181,6 +188,11 @@ public void onBackPressed () { if (c != null) { c.openAlert(R.string.DiscardVideoMessageTitle, R.string.DiscardVideoMessageDescription, Lang.getString(R.string.Discard), (dialog, which) -> closeVideoEditMode(null)); } + } else if (recordMode == RECORD_MODE_AUDIO_EDIT) { + ViewController c = UI.getCurrentStackItem(context); + if (c != null) { + c.openAlert(R.string.DiscardAudioMessageTitle, R.string.DiscardAudioMessageDescription, Lang.getString(R.string.Discard), (dialog, which) -> closeAudioEditMode(null)); + } } else { finishRecording(true); } @@ -205,6 +217,8 @@ public void onActivityDestroy () { if (isOpen()) { if (recordMode == RECORD_MODE_VIDEO_EDIT) { closeVideoEditMode(null); + } else if (recordMode == RECORD_MODE_AUDIO_EDIT) { + closeAudioEditMode(null); } else { stopRecording(CLOSE_MODE_CANCEL, false); } @@ -345,10 +359,8 @@ public void onSpin (CameraControlButton v, float rotate, float scaleFactor) { } });*/ this.switchCameraButton.setIsSmall(); - this.switchCameraButtonWrap = new FrameLayoutFix(context); - Views.setClickable(switchCameraButtonWrap); - RippleSupport.setCircleBackground(switchCameraButtonWrap, 33f, 3f, ColorId.filling, true, null); - this.switchCameraButtonWrap.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(33f) + Screen.dp(3f) * 2, Screen.dp(33f) + Screen.dp(3f) * 2)); + this.switchCameraButtonWrap = new RecordControllerButton(context); + this.switchCameraButtonWrap.init(null); this.switchCameraButtonWrap.setOnClickListener(v -> { if (ownedCamera != null) { ownedCamera.switchCamera(); @@ -357,11 +369,20 @@ public void onSpin (CameraControlButton v, float rotate, float scaleFactor) { this.switchCameraButtonWrap.addView(switchCameraButton); this.rootLayout.addView(switchCameraButtonWrap); + this.disposableSwitchButton = new RecordDisposableSwitchButton(context); + this.disposableSwitchButton.init(null); + this.disposableSwitchButton.setOnClickListener(v -> disposableSwitchButton.toggleActive(true)); + this.rootLayout.addView(disposableSwitchButton); + this.lockView = new RecordLockView(context); Views.setSimpleStateListAnimator(lockView); rootLayout.addView(lockView); this.lockView.setOnClickListener(v -> { - if (isReleased) { + if (recordMode == RECORD_MODE_AUDIO_EDIT) { + resumeRecordingImpl(RECORD_MODE_AUDIO); + } else if (recordMode == RECORD_MODE_VIDEO_EDIT) { + resumeRecordingImpl(RECORD_MODE_VIDEO); + } else if (isReleased) { finishRecording(true); } }); @@ -383,6 +404,17 @@ protected void onLayout (boolean changed, int left, int top, int right, int bott public boolean onTouchEvent (MotionEvent event) { return Views.isValid(this) && super.onTouchEvent(event); } + + @Override + protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { + final int width = MeasureSpec.getSize(widthMeasureSpec); + final int height = MeasureSpec.getSize(heightMeasureSpec); + + final int size = Math.min(Math.min(width - Screen.dp(80), height - Screen.dp(80)), Screen.dp(640)); + final int sizeSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); + + super.onMeasure(sizeSpec, sizeSpec); + } }; this.videoLayout.setOnClickListener(v -> { if (recordMode == RECORD_MODE_VIDEO_EDIT) { @@ -394,16 +426,48 @@ public boolean onTouchEvent (MotionEvent event) { this.videoLayout.setTranslationZ(Screen.dp(1.5f)); this.videoLayout.setElevation(Screen.dp(1f)); } - this.videoLayout.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(200f), Screen.dp(200f), Gravity.CENTER_HORIZONTAL)); + this.videoLayout.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER_HORIZONTAL)); this.rootLayout.addView(videoLayout); this.videoPlaceholderView = new View(context); this.videoPlaceholderView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); this.videoLayout.addView(videoPlaceholderView); - this.progressView = new RoundProgressView(context); - this.progressView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - this.videoLayout.addView(progressView); + this.progressView = new RoundProgressView3(context) { + @Override + protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { + final int width = MeasureSpec.getSize(widthMeasureSpec); + final int height = MeasureSpec.getSize(heightMeasureSpec); + + final int size = Math.min(Math.min(width - Screen.dp(80), height - Screen.dp(80)), Screen.dp(640)) + Screen.dp(RoundProgressView3.PADDING * 2 + 20); + final int sizeSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); + + super.onMeasure(sizeSpec, sizeSpec); + } + }; + this.progressView.setLayoutParams(FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER_HORIZONTAL)); + this.progressView.setDelegate(new RoundProgressView3.Delegate() { + + @Override + public void onTrimSliderDown (RoundProgressView3 view, float start, float end, boolean isEnd) { + videoTimelineView.performSliderDown(isEnd); + } + + @Override + public void onTrimSliderMove (RoundProgressView3 view, float start, float end, boolean isEnd) { + videoTimelineView.performSliderMove(isEnd ? end : start, isEnd); + } + + @Override + public void onTrimSliderUp (RoundProgressView3 view, float start, float end, boolean isEnd) { + videoTimelineView.performSliderUp(isEnd); + } + }); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + this.progressView.setTranslationZ(Screen.dp(2f)); + this.progressView.setElevation(Screen.dp(1.5f)); + } + this.rootLayout.addView(progressView); this.deleteButton = new ImageView(context) { @Override @@ -417,6 +481,8 @@ public boolean onTouchEvent (MotionEvent event) { this.deleteButton.setOnClickListener(v -> { if (recordMode == RECORD_MODE_VIDEO_EDIT) { closeVideoEditMode(null); + } else if (recordMode == RECORD_MODE_AUDIO_EDIT) { + closeAudioEditMode(null); } }); this.deleteButton.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(56f), ViewGroup.LayoutParams.MATCH_PARENT, Gravity.LEFT)); @@ -431,7 +497,11 @@ public boolean onTouchEvent (MotionEvent event) { Views.setClickable(sendButton); this.sendButton.setOnClickListener(v -> { if (!targetController.showSlowModeRestriction(v, null)) { - sendVideo(Td.newSendOptions()); + if (recordMode == RECORD_MODE_VIDEO_EDIT) { + sendVideo(Td.newSendOptions()); + } else if (recordMode == RECORD_MODE_AUDIO_EDIT) { + sendAudio(Td.newSendOptions()); + } } }); this.sendButton.setLayoutParams(FrameLayoutFix.newParams(Screen.dp(55f), ViewGroup.LayoutParams.MATCH_PARENT, Gravity.RIGHT)); @@ -459,6 +529,15 @@ public void onTimelineTrimChanged (VideoTimelineView v, double totalDuration, do videoPreviewView.setTrimRegion(totalDuration, startTimeSeconds, endTimeSeconds); } + @Override + public void onTimelineVisualTrimChanged (VideoTimelineView v, double totalDuration, double startTimeSeconds, double endTimeSeconds) { + if (totalDuration == 0.0) { + progressView.setTrimFactors(0f, 1f); + } else { + progressView.setTrimFactors((float) (startTimeSeconds / totalDuration), (float) (endTimeSeconds / totalDuration)); + } + } + @Override public void onSeekTo (VideoTimelineView v, float progress) { videoPreviewView.seekTo(progress); @@ -470,6 +549,12 @@ public void onSeekTo (VideoTimelineView v, float progress) { this.videoTimelineView.setLayoutParams(params); this.inputOverlayView.addView(videoTimelineView); + params = FrameLayoutFix.newParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + params.leftMargin = params.rightMargin = Screen.dp(56f); + this.audioPreviewView = new VoiceWaveformView(context); + this.audioPreviewView.setLayoutParams(params); + this.inputOverlayView.addView(audioPreviewView); + this.videoPreviewView = new SimpleVideoPlayer(context); this.videoPreviewView.setMuted(true); this.videoPreviewView.setPlaying(true); @@ -512,7 +597,7 @@ protected void onDraw (Canvas c) { } private float getRecordProgress () { - return startTime != 0 ? (float) ((double) (SystemClock.uptimeMillis() - startTime) / (double) MAX_ROUND_DURATION_MS) : 0f; + return startTime != 0 ? (float) ((double) (SystemClock.uptimeMillis() - startTime + lastDuration) / (double) MAX_ROUND_DURATION_MS) : 0f; } @Override @@ -551,6 +636,8 @@ private void updateMiddle () { deleteButton.setTranslationY(editDy); videoTimelineView.setAlpha(editFactor); videoTimelineView.setTranslationY(editDy); + audioPreviewView.setAlpha(editFactor); + audioPreviewView.setTranslationY(editDy); } private void updateTranslations () { @@ -598,7 +685,8 @@ private void updateTranslations () { recordBackground.setTranslationY(cy - recordBackground.getMeasuredHeight() / 2); lockView.setTranslationX(cx - lockView.getMeasuredWidth() / 2); - switchCameraButtonWrap.setTranslationX(cx - switchCameraButtonWrap.getMeasuredWidth() / 2); + switchCameraButtonWrap.setTranslationX(cx - switchCameraButtonWrap.getMeasuredWidth() / 2f); + disposableSwitchButton.setTranslationX(cx - disposableSwitchButton.getMeasuredWidth() / 2f); updateLockY(); if (closeFactor * recordFactor == 1f) { @@ -606,16 +694,53 @@ private void updateTranslations () { } } + private void updateButtons () { + float editFactor = editAnimator.getValue() ? 1f : (recordAnimator.getValue() ? 1f : this.editFactor); + float factor = Math.max(recordFactor, editFactor); + float scale = .6f + .4f * factor; + + lockView.setScaleX(scale); + lockView.setScaleY(scale); + switchCameraButtonWrap.setScaleX(scale); + switchCameraButtonWrap.setScaleY(scale); + disposableSwitchButton.setScaleX(scale); + disposableSwitchButton.setScaleY(scale); + + recordBackground.setExpand(recordFactor); + } + private void updateVideoY () { - int bottom = inputOverlayView.getTop() + overallTranslation; - videoLayout.setTranslationY(bottom / 2 - videoLayout.getLayoutParams().height / 2 + (bottom / 3 * (1f - Math.max(recordFactor, editFactor)))); + float editFactor = recordMode != RECORD_MODE_NONE && editAnimator.getFloatValue() > 0f ? 1f : this.editFactor; + + final int bottom = inputOverlayView.getTop() + overallTranslation; + final float y = bottom / 2f - videoLayout.getMeasuredHeight() / 2f + (bottom / 3f * (1f - Math.max(recordFactor, editFactor))); + + videoLayout.setTranslationY(y); + progressView.setTranslationY(y - Screen.dp(RoundProgressView3.PADDING + 10)); } private void updateLockY () { float cy = voiceVideoButtonView.getTop() + voiceVideoButtonView.getTranslationY() + voiceVideoButtonView.getMeasuredHeight() / 2; - float y = cy - lockView.getMeasuredHeight() - Screen.dp(11f) - Screen.dp(41f) + Screen.dp(24f) * releaseFactor + Screen.dp(24f) * (1f - MathUtils.clamp(recordFactor)); + float y = cy - lockView.getMeasuredHeight() - Screen.dp(11f) - Screen.dp(41f) + Screen.dp(RecordLockView.BUTTON_EXPANDED) * releaseFactor + Screen.dp(24f) * (1f - MathUtils.clamp(recordFactor)); + y -= Screen.dp(12) * editFactor; + lockView.setTranslationY(y); - switchCameraButtonWrap.setTranslationY(y - Screen.dp(16f) - switchCameraButtonWrap.getMeasuredHeight() + Screen.dp(24f) * (1f - MathUtils.clamp(recordFactor)) * (1f - releaseFactor)); + y -= Screen.dp(5) + Screen.dp(50) * (recordingVideo ? lockView.getAlpha() : 1f); + y += Screen.dp(24f) * (1f - MathUtils.clamp(recordFactor)) * (1f - releaseFactor); + + switchCameraButtonWrap.setTranslationY(y); + if (recordMode == RECORD_MODE_VIDEO_EDIT) { + y -= switchCameraButtonWrap.getMeasuredHeight() * switchCameraButtonWrap.getAlpha(); + } else { + if (Views.isValid(switchCameraButtonWrap)) { + y -= switchCameraButtonWrap.getMeasuredHeight(); + } + } + + disposableSwitchButton.setTranslationY(y); + if (Views.isValid(disposableSwitchButton)) { + y -= disposableSwitchButton.getMeasuredHeight(); + } } private boolean isReleased; @@ -657,11 +782,14 @@ private void setOverallTranslation (int translation) { } private void resetViews () { + lastDuration = 0; setTranslations(0f, 0f); switchCameraButton.setCameraIconRes(!Settings.instance().startRoundWithRear()); - progressView.setVisualProgress(0f); + progressView.reset(); durationView.reset(); lockView.setCollapseFactor(0f); + disposableSwitchButton.setActive(false, false); + disposableSwitchButton.setVisibility(canSendSelfDestructMessages() ? View.VISIBLE : View.GONE); recordBackground.setVolume(0f, false); editAnimator.setValue(false, false); videoPreviewView.performDestroy(); @@ -669,6 +797,7 @@ private void resetViews () { videoPreviewView.setMuted(true); videoPreviewView.setPlaying(true); sendButton.destroySlowModeCounterController(); + audioPreviewView.clearData(); setReleased(false, false); resetState(); } @@ -726,13 +855,17 @@ private void checkAxis () { // Duration private long startTime; + private long lastStartDuration; + private long lastDuration; private void startTimers (long ms) { startTime = ms; - durationView.start(startTime); + lastStartDuration = SystemClock.uptimeMillis(); + durationView.start(startTime, lastDuration); } private void stopTimers () { + lastDuration += SystemClock.uptimeMillis() - lastStartDuration; startTime = 0; durationView.stop(); } @@ -800,6 +933,7 @@ private void notifyRecordStateChanged (boolean isRecording) { private static final int RECORD_MODE_AUDIO = 1; private static final int RECORD_MODE_VIDEO = 2; private static final int RECORD_MODE_VIDEO_EDIT = 3; + private static final int RECORD_MODE_AUDIO_EDIT = 4; private static final long EXPAND_DURATION = 160l; private static final long COLLAPSE_DURATION = 140l; @@ -832,7 +966,11 @@ private void updateMuteAlpha () { private void setEditFactor (float factor) { if (this.editFactor != factor) { this.editFactor = factor; + lockView.setEditFactor(factor); updateMainAlphas(); + updateButtons(); + updateMiddle(); + updateLockY(); } } @@ -855,7 +993,7 @@ private static boolean isVideoMode (int mode) { } private static boolean isInRecording (int mode) { - return mode != RECORD_MODE_NONE && mode != RECORD_MODE_VIDEO_EDIT; + return mode != RECORD_MODE_NONE && mode != RECORD_MODE_VIDEO_EDIT && mode != RECORD_MODE_AUDIO_EDIT; } private void setRecordMode (int mode, boolean animated) { @@ -879,6 +1017,10 @@ private void setRecordMode (int mode, boolean animated) { if (isRecording) { voiceVideoButtonView.setInVideoMode(mode == RECORD_MODE_VIDEO, recordFactor > 0f); } + + videoTimelineView.setVisibility(mode == RECORD_MODE_VIDEO_EDIT ? View.VISIBLE : View.GONE); + audioPreviewView.setVisibility(mode == RECORD_MODE_AUDIO_EDIT ? View.VISIBLE : View.GONE); + if (wasRecording != isRecording) { notifyRecordStateChanged(isRecording); if (!isRecording) { @@ -914,6 +1056,8 @@ public boolean startRecording (View view, boolean inRaiseMode) { return false; } + this.savedRoundDurationSeconds = 0; + this.prevVideoPath = null; this.targetChatId = targetController.getChatId(); this.targetMessageThreadId = targetController.getMessageThreadId(); if (needVideo && !tdlib.chatSupportsRoundVideos(targetChatId)) { @@ -978,6 +1122,7 @@ public boolean startRecording (View view, boolean inRaiseMode) { if (inRaiseMode) { lockView.setCollapseFactor(1f); } + lockView.setMode(needVideo ? RecordLockView.MODE_VIDEO : RecordLockView.MODE_AUDIO); setRecordMode(needVideo ? RECORD_MODE_VIDEO : RECORD_MODE_AUDIO, !inRaiseMode); // checkActualRecording(CLOSE_MODE_CANCEL); @@ -994,7 +1139,7 @@ public boolean startRecording (View view, boolean inRaiseMode) { private static final long MINIMUM_AUDIO_RECORDING_DURATION = 500l; private boolean canSendRecording () { - return startTime != 0 && (SystemClock.uptimeMillis() - startTime) >= (recordingVideo ? MINIMUM_VIDEO_RECORDING_DURATION : MINIMUM_AUDIO_RECORDING_DURATION); + return startTime != 0 && (lastDuration + SystemClock.uptimeMillis() - startTime) >= (recordingVideo ? MINIMUM_VIDEO_RECORDING_DURATION : MINIMUM_AUDIO_RECORDING_DURATION); } public boolean finishRecording (boolean needPreview) { @@ -1003,7 +1148,7 @@ public boolean finishRecording (boolean needPreview) { } private boolean stopRecording (int closeMode, boolean showPrompt) { - if (recordMode == RECORD_MODE_NONE || recordMode == RECORD_MODE_VIDEO_EDIT) { + if (recordMode == RECORD_MODE_NONE || recordMode == RECORD_MODE_VIDEO_EDIT || recordMode == RECORD_MODE_AUDIO_EDIT) { return false; } @@ -1016,10 +1161,10 @@ private boolean stopRecording (int closeMode, boolean showPrompt) { int mode = RECORD_MODE_NONE; - boolean async = (closeMode == CLOSE_MODE_PREVIEW || closeMode == CLOSE_MODE_PREVIEW_SCHEDULE) && recordingVideo; + boolean async = (closeMode == CLOSE_MODE_PREVIEW || closeMode == CLOSE_MODE_PREVIEW_SCHEDULE); if (async) { - mode = RECORD_MODE_VIDEO_EDIT; - editAnimator.setValue(true, false); + mode = recordingVideo ? RECORD_MODE_VIDEO_EDIT : RECORD_MODE_AUDIO_EDIT; + editAnimator.setValue(true, true); if (sendButton != null) { sendButton.getSlowModeCounterController(tdlib).setCurrentChat(targetChatId); } @@ -1056,7 +1201,7 @@ private boolean hasValidOutputTarget () { } private void checkActualRecording (int closeMode) { - boolean isRecording = this.recordMode != RECORD_MODE_NONE && this.recordMode != RECORD_MODE_VIDEO_EDIT && gotFocus; + boolean isRecording = this.recordMode != RECORD_MODE_NONE && this.recordMode != RECORD_MODE_VIDEO_EDIT && this.recordMode != RECORD_MODE_AUDIO_EDIT && gotFocus; boolean actuallyRecording = this.currentRecording != RECORD_MODE_NONE; if (!actuallyRecording && isRecording) { switch (recordMode) { @@ -1081,6 +1226,10 @@ private boolean awaitingRoundResult () { return roundCloseMode != CLOSE_MODE_CANCEL; } + private boolean awaitingVoiceResult () { + return voiceCloseMode != CLOSE_MODE_CANCEL; + } + private Throwable releasedTrace; private boolean cleanupVideoPending; @@ -1133,24 +1282,29 @@ private void updateDuration () { } private void updateMainAlphas () { + float editFactor = editAnimator.getValue() ? 1f : (recordAnimator.getValue() ? 1f : this.editFactor); + float range = MathUtils.clamp(recordFactor); float editRange = Math.max(range, editFactor); voiceVideoButtonView.setAlpha(range); inputOverlayView.setAlpha(editRange); - lockView.setAlpha(range); + lockView.setAlpha(editRange); float videoRange = recordingVideo ? editRange : 0f; videoBackgroundView.setFactor(videoRange); videoTopShadowView.setAlpha(videoRange); videoBottomShadowView.setAlpha(videoRange); videoLayout.setAlpha(videoRange); - - progressView.setAlpha(Math.max(recordFactor, 1f - editFactor)); + progressView.setAlpha(videoRange * Math.max(recordFactor, editFactor)); + progressView.setEditFactor(editAnimator.getFloatValue()); float videoScale = .4f + .6f * videoRange; videoLayout.setScaleX(videoScale); videoLayout.setScaleY(videoScale); - switchCameraButtonWrap.setAlpha(recordingVideo ? range : 0); + progressView.setScaleX(videoScale); + progressView.setScaleY(videoScale); + switchCameraButtonWrap.setAlpha(recordingVideo ? editRange : 0); + disposableSwitchButton.setAlpha(editRange); updateMuteAlpha(); @@ -1162,15 +1316,7 @@ private void setRecordFactor (float factor) { this.recordFactor = factor; updateMainAlphas(); - - float scale = .6f + .4f * factor; - lockView.setScaleX(scale); - lockView.setScaleY(scale); - switchCameraButtonWrap.setScaleX(scale); - switchCameraButtonWrap.setScaleY(scale); - - recordBackground.setExpand(factor); - + updateButtons(); updateLockY(); updateDuration(); updateMiddle(); @@ -1261,8 +1407,11 @@ public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator ca case ANIMATOR_EDIT: { if (finalFactor == 0f) { cleanupVideoRecording(); + deleteVoiceRecord(); } else if (finalFactor == 1f && roundCloseMode == CLOSE_MODE_PREVIEW_SCHEDULE) { sendVideo(Td.newSendOptions()); + } else if (finalFactor == 1f && voiceCloseMode == CLOSE_MODE_PREVIEW_SCHEDULE) { + sendAudio(Td.newSendOptions()); } break; } @@ -1279,6 +1428,7 @@ public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator ca private int currentRecording; private int roundCloseMode; + private int voiceCloseMode; private void startRecordingImpl (int mode) { this.currentRecording = mode; @@ -1294,6 +1444,40 @@ private void startRecordingImpl (int mode) { } } + private void resumeRecordingImpl (int mode) { + editAnimator.setValue(false, true); + if (mode == RECORD_MODE_AUDIO) { + currentRecording = mode; + if (tdlib != null) { + Recorder.instance().resume(tdlib, ChatId.isSecret(targetChatId), this); + } + } else if (mode == RECORD_MODE_VIDEO) { + if (recordingRoundVideo) { + cleanupVideoPending = true; + return; + } + + ownedCamera.getLegacyManager().setUseRoundRender(false); + + recordMode = RECORD_MODE_NONE; + resetRoundState(); + // editFactor = 0f; + + prevVideoHasTrim = videoPreviewView.hasTrim(); + prevVideoTrimStart = videoPreviewView.getStartTime(); + prevVideoTrimEnd = videoPreviewView.getEndTime(); + + videoPreviewView.performDestroy(); + videoPreviewView.setMuted(true); + videoPreviewView.setPlaying(true); + resetRoundState(); + isCameraReady = false; + + prepareVideoRecording(); + } + setRecordMode(mode, true); + } + private MessagesController findMessagesController () { ViewController c = UI.getCurrentStackItem(context); return c instanceof MessagesController ? (MessagesController) c : null; @@ -1303,13 +1487,9 @@ private void stopRecordingImpl (int closeMode) { final boolean needResult = closeMode != CLOSE_MODE_CANCEL && hasValidOutputTarget(); switch (currentRecording) { case RECORD_MODE_AUDIO: + voiceCloseMode = closeMode; if (needResult) { - if (closeMode == CLOSE_MODE_PREVIEW || closeMode == CLOSE_MODE_PREVIEW_SCHEDULE) { - targetController.prepareVoicePreview((int) ((SystemClock.uptimeMillis() - startTime) / 1000l)); - // TODO stop playing temporary record - // Player.instance().destroy(); - } - Recorder.instance().save(); + Recorder.instance().save(closeMode == CLOSE_MODE_PREVIEW || closeMode == CLOSE_MODE_PREVIEW_SCHEDULE); } else { Recorder.instance().cancel(); } @@ -1348,15 +1528,38 @@ public void onFail () { } } + private TGRecord voiceRecord; + + private void deleteVoiceRecord () { + if (voiceRecord != null && !awaitingVoiceResult()) { + voiceRecord = null; + } + } + @Override public void onSave (final Tdlib.Generation generation, final int duration, final byte[] waveform) { UI.post(() -> { - if (hasValidOutputTarget()) { - targetController.shareItem(new TGRecord(tdlib, generation, duration, waveform)); + final TGRecord record = new TGRecord(tdlib, generation, duration, waveform); + if (awaitingVoiceResult()) { + if (voiceCloseMode == CLOSE_MODE_SEND) { + sendAudioNote(new TdApi.InputMessageVoiceNote(record.toInputFile(), record.getDuration(), record.getWaveform(), null, obtainSelfDestructType()), Td.newSendOptions()); + } else { + audioPreviewView.processRecord(voiceRecord = record); + } } }); } + private TdApi.MessageSelfDestructType obtainSelfDestructType () { + return canSendSelfDestructMessages() && disposableSwitchButton != null && disposableSwitchButton.isActive() ? + new TdApi.MessageSelfDestructTypeImmediately() : null; + } + + private boolean canSendSelfDestructMessages () { + return tdlib.selfChatId() != targetChatId && ChatId.isUserChat(targetChatId); + } + + // Video record impl private void setupCamera (boolean isOwned) { @@ -1497,7 +1700,7 @@ public void onVideoRecordingStarted (String key, long startTimeMs) { @Override public void onVideoRecordProgress (String key, long readyBytesCount) { - if (StringUtils.equalsOrBothEmpty(roundKey, key)) { + if (StringUtils.equalsOrBothEmpty(roundKey, key) && prevVideoPath == null) { tdlib.client().send(new TdApi.SetFileGenerationProgress(roundGenerationId, 0, readyBytesCount), tdlib.silentHandler()); } } @@ -1524,7 +1727,34 @@ private void sendVideoNote (TdApi.InputMessageVideoNote videoNote, TdApi.Message cleanupVideoRecording(); } + private void sendAudioNote (TdApi.InputMessageVoiceNote voiceNote, TdApi.MessageSendOptions initialSendOptions) { + if (hasValidOutputTarget()) { + targetController.pickDateOrProceed(initialSendOptions, (modifiedSendOptions, disableMarkdown) -> { + long chatId = targetController.getChatId(); + long messageThreadId = targetController.getMessageThreadId(); + TdApi.InputMessageReplyTo replyTo = targetController.obtainReplyTo(); + final TdApi.MessageSendOptions finalSendOptions = Td.newSendOptions(initialSendOptions, tdlib.chatDefaultDisableNotifications(chatId)); + tdlib.sendMessage(chatId, messageThreadId, replyTo, finalSendOptions, voiceNote, null); + }); + } + + voiceCloseMode = CLOSE_MODE_CANCEL; + deleteVoiceRecord(); + } + private void finishFileGeneration (long resultFileSize) { + if (prevVideoPath != null) { + Background.instance().post(() -> { + try { + VideoGen.appendTwoVideos(prevVideoPath, roundOutputPath, roundOutputPath + ".merge", prevVideoHasTrim, prevVideoTrimStart, prevVideoTrimEnd); + } catch (Exception e) { + throw new RuntimeException(e); + } + U.moveFile(new File(roundOutputPath + ".merge"), new File(roundOutputPath)); + tdlib.client().send(new TdApi.FinishFileGeneration(roundGenerationId, null), tdlib.silentHandler()); + }); + return; + } tdlib.client().send(new TdApi.SetFileGenerationProgress(roundGenerationId, resultFileSize, resultFileSize), tdlib.silentHandler()); tdlib.client().send(new TdApi.FinishFileGeneration(roundGenerationId, null), tdlib.silentHandler()); } @@ -1537,13 +1767,13 @@ public void onVideoRecordingFinished (String key, long resultFileSize, long resu boolean success = resultFileSize > 0; if (awaitingRoundResult()) { if (success) { - this.savedRoundDurationSeconds = (int) resultFileDurationUnit.toSeconds(resultFileDuration); + this.savedRoundDurationSeconds += (int) resultFileDurationUnit.toSeconds(resultFileDuration); if (roundCloseMode == CLOSE_MODE_PREVIEW || roundCloseMode == CLOSE_MODE_PREVIEW_SCHEDULE) { awaitRoundVideo(); finishFileGeneration(resultFileSize); } else { finishFileGeneration(resultFileSize); - sendVideoNote(new TdApi.InputMessageVideoNote(new TdApi.InputFileId(roundFile.id), null, savedRoundDurationSeconds, VIDEO_NOTE_LENGTH, null), Td.newSendOptions(), roundFile); + sendVideoNote(new TdApi.InputMessageVideoNote(new TdApi.InputFileId(roundFile.id), null, savedRoundDurationSeconds, VIDEO_NOTE_LENGTH, obtainSelfDestructType()), Td.newSendOptions(), roundFile); } } else { finishFileGeneration(-1); @@ -1616,6 +1846,11 @@ private boolean roundFileReceived () { return roundGenerationFinished && TD.isFileLoaded(roundFile); } + private String prevVideoPath; + private double prevVideoTrimStart; + private double prevVideoTrimEnd; + private boolean prevVideoHasTrim; + private void onRoundVideoReady () { if (!roundFileReceived()) { return; @@ -1624,6 +1859,7 @@ private void onRoundVideoReady () { tdlib.files().unsubscribe(roundFile.id, this); videoTimelineView.setVideoPath(roundFile.local.path); videoPreviewView.setVideo(roundFile.local.path); + prevVideoPath = roundFile.local.path; if (scheduledEditClose) { scheduledEditClose = false; @@ -1674,9 +1910,9 @@ private void closeVideoEditMode (@NonNull TdApi.MessageSendOptions initialSendOp 0 ); TdApi.InputFileGenerated trimmedFile = new TdApi.InputFileGenerated(roundFile.local.path, conversion, 0); - sendVideoNote(new TdApi.InputMessageVideoNote(trimmedFile, null, (int) Math.round(endTimeSeconds - startTimeSeconds), VIDEO_NOTE_LENGTH, null), initialSendOptions, null); + sendVideoNote(new TdApi.InputMessageVideoNote(trimmedFile, null, (int) Math.round(endTimeSeconds - startTimeSeconds), VIDEO_NOTE_LENGTH, obtainSelfDestructType()), initialSendOptions, null); } else { - sendVideoNote(new TdApi.InputMessageVideoNote(new TdApi.InputFileId(roundFile.id), null, savedRoundDurationSeconds, VIDEO_NOTE_LENGTH, null), initialSendOptions, roundFile); + sendVideoNote(new TdApi.InputMessageVideoNote(new TdApi.InputFileId(roundFile.id), null, savedRoundDurationSeconds, VIDEO_NOTE_LENGTH, obtainSelfDestructType()), initialSendOptions, roundFile); } } else { tdlib.client().send(new TdApi.DeleteFile(roundFile.id), tdlib.silentHandler()); @@ -1686,4 +1922,41 @@ private void closeVideoEditMode (@NonNull TdApi.MessageSendOptions initialSendOp editAnimator.setValue(false, true); sendButton.destroySlowModeCounterController(); } + + // Audio Preview + + private void sendAudio (@NonNull TdApi.MessageSendOptions initialSendOptions) { + if (recordMode == RECORD_MODE_AUDIO_EDIT) { + closeAudioEditMode(initialSendOptions); + } + } + + private void closeAudioEditMode (TdApi.MessageSendOptions initialSendOptions) { + if (recordMode != RECORD_MODE_AUDIO_EDIT) { + return; + } + + this.recordMode = RECORD_MODE_NONE; + + final var record = voiceRecord; + if (initialSendOptions != null && record != null) { + if (record.getWaveform() == null) { + Background.instance().post(() -> { + byte[] waveform = N.getWaveform(record.getPath()); + sendAudioNote(new TdApi.InputMessageVoiceNote(record.toInputFile(), record.getDuration(), waveform, null, obtainSelfDestructType()), initialSendOptions); + }); + } else { + sendAudioNote(new TdApi.InputMessageVoiceNote(record.toInputFile(), record.getDuration(), record.getWaveform(), null, obtainSelfDestructType()), initialSendOptions); + } + Recorder.instance().finish(false); + } else { + if (record != null) { + tdlib.client().send(new TdApi.DeleteFile(record.getFileId()), tdlib.silentHandler()); + } + voiceCloseMode = CLOSE_MODE_CANCEL; + Recorder.instance().finish(true); + } + editAnimator.setValue(false, true); + sendButton.destroySlowModeCounterController(); + } } diff --git a/app/src/main/java/org/thunderdog/challegram/player/RecordControllerButton.java b/app/src/main/java/org/thunderdog/challegram/player/RecordControllerButton.java new file mode 100644 index 0000000000..db2905bfdf --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/player/RecordControllerButton.java @@ -0,0 +1,112 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 30/06/2024 + */ +package org.thunderdog.challegram.player; + +import android.content.Context; +import android.graphics.Canvas; + +import androidx.annotation.NonNull; + +import org.thunderdog.challegram.navigation.ViewController; +import org.thunderdog.challegram.support.RippleSupport; +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.Views; + +import me.vkryl.android.AnimatorUtils; +import me.vkryl.android.animator.BoolAnimator; +import me.vkryl.android.animator.FactorAnimator; +import me.vkryl.android.widget.FrameLayoutFix; +import me.vkryl.core.ColorUtils; + +public class RecordControllerButton extends FrameLayoutFix { + public static final int BUTTON_SIZE = 40; + public static final int PADDING = 5; + + private ViewController themeProvider; + + public RecordControllerButton (Context context) { + super(context); + + Views.setClickable(this); + } + + public void init (ViewController themeProvider) { + this.themeProvider = themeProvider; + RippleSupport.setCircleBackground(RecordControllerButton.this, BUTTON_SIZE, PADDING, ColorId.filling, true, themeProvider); + } + + @Override + protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { + final int sizeMeasureSpec = MeasureSpec.makeMeasureSpec(Screen.dp(BUTTON_SIZE + PADDING * 2), MeasureSpec.EXACTLY); + super.onMeasure(sizeMeasureSpec, sizeMeasureSpec); + } + + + @Override + protected void dispatchDraw (@NonNull Canvas canvas) { + final float active = isActiveAnimator.getFloatValue(); + if (active > 0f) { + final int color = ColorUtils.fromToArgb(Theme.getColor(ColorId.filling), Theme.getColor(ColorId.fillingPositive), active); + canvas.drawCircle(getMeasuredWidth() / 2f, getMeasuredHeight() / 2f, Screen.dp(BUTTON_SIZE / 2f), Paints.fillingPaint(color)); + } + + super.dispatchDraw(canvas); + } + + + + + + private final FactorAnimator.Target isActiveTarget = new FactorAnimator.Target() { + @Override + public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { + invalidate(); + onActiveFactorChanged(factor); + } + + @Override + public void onFactorChangeFinished (int id, float finalFactor, FactorAnimator callee) { + onActiveFactorChangeFinished(finalFactor); + } + }; + private final BoolAnimator isActiveAnimator = new BoolAnimator(0, isActiveTarget, AnimatorUtils.DECELERATE_INTERPOLATOR, 220L); + + public void setActive (boolean active, boolean animated) { + isActiveAnimator.setValue(active, animated); + } + + public void toggleActive (boolean animated) { + isActiveAnimator.setValue(!isActiveAnimator.getValue(), animated); + } + + public boolean isActive () { + return isActiveAnimator.getValue(); + } + + public float getActiveFactor () { + return isActiveAnimator.getFloatValue(); + } + + protected void onActiveFactorChanged (float factor) { + + } + + protected void onActiveFactorChangeFinished (float factor) { + + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/player/RecordDisposableSwitchButton.java b/app/src/main/java/org/thunderdog/challegram/player/RecordDisposableSwitchButton.java new file mode 100644 index 0000000000..5fb0a89f92 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/player/RecordDisposableSwitchButton.java @@ -0,0 +1,58 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 30/06/2024 + */ +package org.thunderdog.challegram.player; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; + +import androidx.annotation.NonNull; + +import org.thunderdog.challegram.R; +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.tool.Drawables; +import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.PorterDuffPaint; + +import me.vkryl.core.ColorUtils; + +public class RecordDisposableSwitchButton extends RecordControllerButton { + private final Drawable drawable; + + public RecordDisposableSwitchButton(Context context) { + super(context); + drawable = Drawables.get(context.getResources(), R.drawable.baseline_hot_once_24); + } + + @Override + protected void dispatchDraw (@NonNull Canvas canvas) { + super.dispatchDraw(canvas); + + final float cx = getMeasuredWidth() / 2f; + final float cy = getMeasuredHeight() / 2f; + final float active = getActiveFactor(); + + if (active == 0f) { + Drawables.drawCentered(canvas, drawable, cx, cy, PorterDuffPaint.get(ColorId.icon)); + } else if (active == 1f) { + Drawables.drawCentered(canvas, drawable, cx, cy, PorterDuffPaint.get(ColorId.fillingPositiveContent)); + } else { + Drawables.drawCentered(canvas, drawable, cx, cy, Paints.getPorterDuffPaint( + ColorUtils.fromToArgb(Theme.getColor(ColorId.icon), Theme.getColor(ColorId.fillingPositiveContent), active) + )); + } + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/player/RecordDurationView.java b/app/src/main/java/org/thunderdog/challegram/player/RecordDurationView.java index 10f524e5ef..d5e79556fa 100644 --- a/app/src/main/java/org/thunderdog/challegram/player/RecordDurationView.java +++ b/app/src/main/java/org/thunderdog/challegram/player/RecordDurationView.java @@ -60,7 +60,8 @@ public void setTimerCallback (TimerCallback timerCallback) { // Public interrupts - public void start (long startTimeMs) { + public void start (long startTimeMs, long additionalDuration) { + this.additionalDuration = additionalDuration; if (animator == null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { final android.animation.TimeAnimator animator; @@ -69,7 +70,7 @@ public void start (long startTimeMs) { elapsedDeltaTime += deltaTime; if (elapsedDeltaTime >= 15) { elapsedDeltaTime = 0; - if (processMillis(totalTime)) { + if (processMillis(totalTime + this.additionalDuration)) { invalidate(); if (timerCallback != null) { timerCallback.onTimerTick(); @@ -87,7 +88,7 @@ public void start (long startTimeMs) { long totalTime = SystemClock.uptimeMillis() - animationStart; if (elapsedDeltaTime == 0l || totalTime - elapsedDeltaTime >= 15l) { elapsedDeltaTime = totalTime; - if (processMillis(totalTime)) { + if (processMillis(totalTime + this.additionalDuration)) { invalidate(); if (timerCallback != null) { timerCallback.onTimerTick(); @@ -159,6 +160,7 @@ public void draw (Canvas c, float centerY) { } private static final float SWITCH_FACTOR = .125f; + private long additionalDuration; private boolean processMillis (long timeTotal) { int updated = 0; diff --git a/app/src/main/java/org/thunderdog/challegram/player/RecordLockView.java b/app/src/main/java/org/thunderdog/challegram/player/RecordLockView.java index 23c7332b2e..e417da8c64 100644 --- a/app/src/main/java/org/thunderdog/challegram/player/RecordLockView.java +++ b/app/src/main/java/org/thunderdog/challegram/player/RecordLockView.java @@ -19,38 +19,60 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.RectF; +import android.graphics.drawable.Drawable; import android.os.Build; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import org.thunderdog.challegram.R; import org.thunderdog.challegram.theme.ColorId; import org.thunderdog.challegram.theme.Theme; import org.thunderdog.challegram.tool.DrawAlgorithms; +import org.thunderdog.challegram.tool.Drawables; import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.PorterDuffPaint; import org.thunderdog.challegram.tool.Screen; import org.thunderdog.challegram.tool.Views; import me.vkryl.core.ColorUtils; +import me.vkryl.core.MathUtils; public class RecordLockView extends View { + public static final int BUTTON_SIZE = RecordControllerButton.BUTTON_SIZE; + public static final int BUTTON_EXPANDED = 33; + + private final Drawable drawableVoice; + private final Drawable drawableRound; + public RecordLockView (Context context) { super(context); + + drawableVoice = Drawables.get(context.getResources(), R.drawable.baseline_mic_24); + drawableRound = Drawables.get(context.getResources(), R.drawable.deproko_baseline_msg_video_24); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { setOutlineProvider(new android.view.ViewOutlineProvider() { @Override @TargetApi(Build.VERSION_CODES.LOLLIPOP) public void getOutline (View view, android.graphics.Outline outline) { - outline.setRoundRect(0, 0, view.getMeasuredWidth(), (int) (view.getMeasuredHeight() - Screen.dp(33f) * collapseFactor), Screen.dp(33f) / 2); + outline.setRoundRect(0, 0, view.getMeasuredWidth(), (int) (view.getMeasuredHeight() - Screen.dp(BUTTON_EXPANDED) * collapseFactor), Screen.dp(BUTTON_SIZE / 2f)); } }); } - setLayoutParams(new ViewGroup.LayoutParams(Screen.dp(33f), Screen.dp(66f))); + setLayoutParams(new ViewGroup.LayoutParams(Screen.dp(BUTTON_SIZE), Screen.dp(BUTTON_SIZE + BUTTON_EXPANDED))); } @Override public boolean onTouchEvent (MotionEvent e) { + if (e.getAction() == MotionEvent.ACTION_DOWN) { + float bottom = getMeasuredHeight() - Screen.dp(BUTTON_EXPANDED) * collapseFactor; + if (e.getY() > bottom) { + return false; + } + } + return Views.onTouchEvent(this, e) && super.onTouchEvent(e); } @@ -62,7 +84,7 @@ protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { } private float getCenterY () { - return (int) (getMeasuredHeight() - Screen.dp(33f) * collapseFactor) - Screen.dp(33f) / 2; + return (int) (getMeasuredHeight() - Screen.dp(BUTTON_EXPANDED) * collapseFactor) - Screen.dp(BUTTON_SIZE / 2f); } private float collapseFactor; @@ -87,6 +109,28 @@ public void setSendFactor (float sendFactor) { } } + private float editFactor; + + public void setEditFactor (float editFactor) { + if (this.editFactor != editFactor) { + this.editFactor = editFactor; + invalidate(); + } + } + + public static final int MODE_DEFAULT = 0; + public static final int MODE_AUDIO = 1; + public static final int MODE_VIDEO = 2; + + private int mode = MODE_DEFAULT; + + public void setMode (int mode) { + this.mode = mode; + } + + + private static final RectF tmpRect = new RectF(); + @Override protected void onDraw (Canvas c) { int fillingColor = Theme.fillingColor(); @@ -94,14 +138,14 @@ protected void onDraw (Canvas c) { RectF rectF = Paints.getRectF(); final int viewWidth = getMeasuredWidth(); final int viewHeight = getMeasuredHeight(); - rectF.set(0, 0, viewWidth, viewHeight - Screen.dp(33f) * collapseFactor); - int radius = Screen.dp(33f) / 2; + rectF.set(0, 0, viewWidth, viewHeight - Screen.dp(BUTTON_EXPANDED) * collapseFactor); + int radius = Screen.dp(BUTTON_SIZE) / 2; c.drawRoundRect(rectF, radius, radius, Paints.fillingPaint(fillingColor)); int bottomCy = (int) rectF.bottom - radius; int cx = viewWidth / 2; - int cy = Screen.dp(33f) / 2; + int cy = Screen.dp(BUTTON_SIZE) / 2; final int grayColor = Theme.iconColor(); final int redColor = Theme.getColor(ColorId.iconNegative); @@ -109,13 +153,34 @@ protected void onDraw (Canvas c) { int totalDy = (int) (Screen.dp(2f) * collapseFactor * (1f - sendFactor)); int width = (int) (Screen.dp(6f) + Screen.dp(2f) * (1f - sendFactor)); - int height = (int) (Screen.dp(6f) + Screen.dp(1f) * (1f - sendFactor)); - int dy = (int) (Screen.dp(33f) /3 * (1f - collapseFactor)); + int height = (int) (Screen.dp(6f) + Screen.dp(1f) * (mode == MODE_DEFAULT ? (1f - sendFactor) : 1f)); + int dy = (int) (Screen.dp(BUTTON_SIZE) / 3f * (1f - collapseFactor)); rectF.set(cx - width, cy - height + dy + totalDy, cx + width, cy + height + dy + totalDy); - c.drawRoundRect(rectF, Screen.dp(2f), Screen.dp(2f), Paints.fillingPaint(ColorUtils.fromToArgb(grayColor, redColor, sendFactor))); + + final float r = Screen.dp(2) * (1f - sendFactor); + if (mode == MODE_DEFAULT) { + c.drawRoundRect(rectF, Screen.dp(2f), Screen.dp(2f), Paints.fillingPaint(ColorUtils.fromToArgb(grayColor, redColor, sendFactor))); + } else { + final float pauseAlpha = 1f - editFactor; + final float alpha = editFactor; + + if (editFactor < 1f) { + c.drawRoundRect(rectF, r, r, Paints.fillingPaint(ColorUtils.alphaColor(pauseAlpha, grayColor))); + final float w = Screen.dp(2); + final float h = MathUtils.fromTo(Screen.dp(2), rectF.height() / 2f + 1, sendFactor); + tmpRect.set(cx - w, rectF.centerY() - h, cx + w, rectF.centerY() + h); + c.drawRoundRect(tmpRect, r, r, Paints.fillingPaint(ColorUtils.alphaColor(pauseAlpha, fillingColor))); + } + + if (editFactor > 0f) { + Drawables.drawCentered(c, mode == MODE_VIDEO ? drawableRound : drawableVoice, rectF.centerX(), rectF.centerY(), PorterDuffPaint.get(ColorId.icon, alpha)); + } + } if (sendFactor < 1f) { - c.drawCircle(cx, rectF.centerY(), Screen.dp(2f), Paints.fillingPaint(ColorUtils.alphaColor(1f - sendFactor, fillingColor))); + if (mode == MODE_DEFAULT) { + c.drawCircle(cx, rectF.centerY(), Screen.dp(2f), Paints.fillingPaint(ColorUtils.alphaColor(1f - sendFactor, fillingColor))); + } dy /= 2; rectF.offset(0, -dy); Paint paint = Paints.strokeBigPaint(ColorUtils.alphaColor(1f - sendFactor, grayColor)); diff --git a/app/src/main/java/org/thunderdog/challegram/player/RoundProgressView3.java b/app/src/main/java/org/thunderdog/challegram/player/RoundProgressView3.java new file mode 100644 index 0000000000..7cbaf2188c --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/player/RoundProgressView3.java @@ -0,0 +1,254 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 24/06/2024 + */ +package org.thunderdog.challegram.player; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.view.MotionEvent; +import android.view.View; + +import androidx.annotation.NonNull; + +import org.thunderdog.challegram.theme.ColorId; +import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.tool.Paints; +import org.thunderdog.challegram.tool.Screen; + +import me.vkryl.core.ColorUtils; +import me.vkryl.core.MathUtils; + +public class RoundProgressView3 extends View { + public static final int PADDING = 16; + private static final int STROKE_WIDTH = 4; + private static final int POINTER_RADIUS = 6; + + private final Paint strokePaintDefault; + private final Paint strokePaintFilling; + + public RoundProgressView3(Context context) { + super(context); + strokePaintDefault = new Paint(Paints.videoStrokePaint()); + strokePaintFilling = new Paint(Paints.videoStrokePaint()); + } + + private float visualProgress; + + public void setVisualProgress (float progress) { + if (this.visualProgress != progress) { + this.visualProgress = progress; + invalidate(); + } + } + + + + + private float lastDrawProgress; + private float lastDrawStartPointerX, lastDrawStartPointerY, lastDrawEndPointerX, lastDrawEndPointerY; + + @Override + protected void onDraw (@NonNull Canvas c) { + final int viewWidth = getMeasuredWidth(); + final int viewHeight = getMeasuredHeight(); + final float progress = visualProgress; + + final int defaultColor = 0xFFFFFFFF; + final int fillingColor = ColorUtils.fromToArgb(defaultColor, Theme.getColor(ColorId.fillingPositive), editFactor); + + strokePaintDefault.setStrokeWidth(Screen.dp(STROKE_WIDTH)); + strokePaintDefault.setColor(defaultColor); + + strokePaintFilling.setStrokeWidth(Screen.dp(STROKE_WIDTH)); + strokePaintFilling.setColor(fillingColor); + + + final int padding = Screen.dp(PADDING); + final float angle = 360f * progress; + + RectF rectF = Paints.getRectF(); + rectF.set(padding, padding, viewWidth - padding, viewHeight - padding); + + c.drawArc(rectF, -90, angle * trimStartFactor, false, strokePaintDefault); + c.drawArc(rectF, -90 + angle * trimEndFactor, angle * (1f - trimEndFactor), false, strokePaintDefault); + c.drawArc(rectF, -90 + angle * trimStartFactor, angle * (trimEndFactor - trimStartFactor), false, strokePaintFilling); + + { + final double angleRad = Math.toRadians(-90 + angle * trimStartFactor); + float x = lastDrawStartPointerX = (float) Math.cos(angleRad) * rectF.width() / 2f + rectF.centerX(); + float y = lastDrawStartPointerY = (float) Math.sin(angleRad) * rectF.width() / 2f + rectF.centerY(); + c.drawCircle(x, y, Screen.dp(POINTER_RADIUS) * editFactor, Paints.fillingPaint(fillingColor)); + } + + { + final double angleRad = Math.toRadians(-90 + angle * trimEndFactor); + float x = lastDrawEndPointerX = (float) Math.cos(angleRad) * rectF.width() / 2f + rectF.centerX(); + float y = lastDrawEndPointerY = (float) Math.sin(angleRad) * rectF.width() / 2f + rectF.centerY(); + c.drawCircle(x, y, Screen.dp(POINTER_RADIUS) * editFactor, Paints.fillingPaint(fillingColor)); + } + + this.lastDrawProgress = progress; + } + + + + private float editFactor; + + public void setEditFactor (float editFactor) { + if (this.editFactor != editFactor){ + this.editFactor = editFactor; + invalidate(); + } + } + + private float trimStartFactor = 0f, trimEndFactor = 1f; + + public void setTrimFactors (float start, float end) { + if (captured != CAPTURED_NONE) { + return; + } + + this.trimStartFactor = start; + this.trimEndFactor = end; + invalidate(); + } + + public void reset () { + this.trimStartFactor = 0f; + this.trimEndFactor = 1f; + this.visualProgress = 0f; + this.editFactor = 0f; + } + + + + + + public interface Delegate { + void onTrimSliderDown (RoundProgressView3 view, float start, float end, boolean isEnd); + void onTrimSliderMove (RoundProgressView3 view, float start, float end, boolean isEnd); + void onTrimSliderUp (RoundProgressView3 view, float start, float end, boolean isEnd); + } + + + private static final int CAPTURED_NONE = 0; + private static final int CAPTURED_START = 1; + private static final int CAPTURED_END = 2; + + private Delegate delegate; + private int captured; + + public void setDelegate (Delegate delegate) { + this.delegate = delegate; + } + + @Override + public boolean onTouchEvent (MotionEvent event) { + final int action = event.getAction(); + final float x = event.getX(); + final float y = event.getY(); + + if (editFactor == 0) { + captured = CAPTURED_NONE; + return false; + } + + switch (action) { + case MotionEvent.ACTION_DOWN: + if (MathUtils.distance(x, y, lastDrawEndPointerX, lastDrawEndPointerY) < Screen.dp(24)) { + captured = CAPTURED_END; + if (delegate != null) { + delegate.onTrimSliderDown(this, trimStartFactor, trimEndFactor, true); + } + return true; + } + if (MathUtils.distance(x, y, lastDrawStartPointerX, lastDrawStartPointerY) < Screen.dp(24)) { + captured = CAPTURED_START; + if (delegate != null) { + delegate.onTrimSliderDown(this, trimStartFactor, trimEndFactor, false); + } + return true; + } + break; + case MotionEvent.ACTION_MOVE: + final double rad = Math.atan2(y - getMeasuredHeight() / 2f, x - getMeasuredWidth() / 2f); + float angle = ((float) Math.toDegrees(rad) + 360 + 90) % 360; + + if (captured == CAPTURED_END) { + final float angleStart = 360 * visualProgress * trimStartFactor; + final float angleEnd = 360 * visualProgress; + + if (!(angleStart <= angle && angle <= angleEnd)) { + final float distanceToStart = angleDistance(angleStart, angle); + final float distanceToEnd = angleDistance(angleEnd, angle); + angle = (distanceToStart < distanceToEnd) ? angleStart : angleEnd; + } + + trimEndFactor = angle / (360f * visualProgress); + if (delegate != null) { + delegate.onTrimSliderMove(this, trimStartFactor, trimEndFactor, true); + } + invalidate(); + return true; + } + + if (captured == CAPTURED_START) { + final float angleStart = 0; + final float angleEnd = 360 * visualProgress * trimEndFactor; + + if (!(angleStart <= angle && angle <= angleEnd)) { + final float distanceToStart = angleDistance(angleStart, angle); + final float distanceToEnd = angleDistance(angleEnd, angle); + angle = (distanceToStart < distanceToEnd) ? angleStart : angleEnd; + } + + trimStartFactor = angle / (360f * visualProgress); + if (delegate != null) { + delegate.onTrimSliderMove(this, trimStartFactor, trimEndFactor, false); + } + invalidate(); + return true; + } + + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (captured != CAPTURED_NONE) { + /*updateSeekRunnable.cancelIfScheduled(); + if (controller != null) { + controller.seekTo(MathUtils.clamp(visualProgress), true); + } + if (seekCaught != null) { + seekCaught.requestDisallowInterceptTouchEvent(false); + }*/ + + if (delegate != null) { + delegate.onTrimSliderUp(this, trimStartFactor, trimEndFactor, captured == CAPTURED_END); + } + captured = CAPTURED_NONE; + return true; + } + } + + return false; + } + + private static float angleDistance (float angle1, float angle2) { + final float distance = Math.abs(angle1 - angle2); + return distance > 180 ? 360 - distance : distance; + } + +} diff --git a/app/src/main/java/org/thunderdog/challegram/player/RoundVideoController.java b/app/src/main/java/org/thunderdog/challegram/player/RoundVideoController.java index 796deb3c4c..84013cf0f4 100644 --- a/app/src/main/java/org/thunderdog/challegram/player/RoundVideoController.java +++ b/app/src/main/java/org/thunderdog/challegram/player/RoundVideoController.java @@ -318,7 +318,7 @@ public void onFileGenerationFinished (@NonNull TdApi.File file) { private boolean isRendered; - private static final boolean USE_SURFACE = false; // Build.VERSION.SDK_INT >= Build.VERSION_CODES.N + public static final boolean USE_SURFACE = false; // Build.VERSION.SDK_INT >= Build.VERSION_CODES.N private void preparePlayerIfNeeded () { // Prepare off-screen texture if (texturePrepared) { diff --git a/app/src/main/java/org/thunderdog/challegram/player/RoundVideoRecorder.java b/app/src/main/java/org/thunderdog/challegram/player/RoundVideoRecorder.java index 5b4dd8a483..9b234d4119 100644 --- a/app/src/main/java/org/thunderdog/challegram/player/RoundVideoRecorder.java +++ b/app/src/main/java/org/thunderdog/challegram/player/RoundVideoRecorder.java @@ -152,7 +152,7 @@ public void openCamera () { private volatile boolean isSwitchingToNewCamera; public boolean canSwitchToNewCamera () { - return !isSwitchingToNewCamera && initied && eglSurface != null; + return !isSwitchingToNewCamera && initied; // && eglSurface != null; } public void switchToNewCamera () { diff --git a/app/src/main/java/org/thunderdog/challegram/player/TGPlayerController.java b/app/src/main/java/org/thunderdog/challegram/player/TGPlayerController.java index d915fe1816..335ddff9f0 100644 --- a/app/src/main/java/org/thunderdog/challegram/player/TGPlayerController.java +++ b/app/src/main/java/org/thunderdog/challegram/player/TGPlayerController.java @@ -28,6 +28,7 @@ import org.thunderdog.challegram.config.Config; import org.thunderdog.challegram.data.InlineResult; import org.thunderdog.challegram.data.TD; +import org.thunderdog.challegram.data.TGMessage; import org.thunderdog.challegram.telegram.GlobalMessageListener; import org.thunderdog.challegram.telegram.Tdlib; import org.thunderdog.challegram.telegram.TdlibManager; @@ -1769,6 +1770,7 @@ public void handleMessage (Message msg) { public static final int PAUSE_REASON_RECORD_VIDEO = 1 << 8; public static final int PAUSE_REASON_PROXIMITY = 1 << 9; public static final int PAUSE_REASON_OPEN_WEB_VIDEO = 1 << 10; + public static final int PAUSE_REASON_OPEN_ONCE_MEDIA = 1 << 11; private boolean needResume; private int pauseReasons; @@ -1829,8 +1831,22 @@ public static class PlayList { * @param originIndex index of message requested in {@link PlayListBuilder#buildPlayList(TdApi.Message)} */ public PlayList (List messages, int originIndex) { - this.messages = messages; - this.originIndex = originIndex; + this.messages = new ArrayList<>(); + + int newOriginIndex = -1; + for (int a = 0; a < messages.size(); a++) { + final TdApi.Message msg = messages.get(a); + final boolean isOrigin = a == originIndex; + // FIXME: this should be filtered on the PlayListBuilder implementation level + if (TD.isSelfDestructTypeImmediately(msg) && !isOrigin) { + continue; + } + if (isOrigin) { + newOriginIndex = this.messages.size(); + } + this.messages.add(msg); + } + this.originIndex = newOriginIndex; } /** diff --git a/app/src/main/java/org/thunderdog/challegram/player/VoiceWaveformView.java b/app/src/main/java/org/thunderdog/challegram/player/VoiceWaveformView.java new file mode 100644 index 0000000000..22d37b7cd4 --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/player/VoiceWaveformView.java @@ -0,0 +1,127 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 01/07/2024 + */ +package org.thunderdog.challegram.player; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.view.View; +import android.view.animation.AnticipateOvershootInterpolator; + +import androidx.annotation.NonNull; + +import org.thunderdog.challegram.N; +import org.thunderdog.challegram.component.chat.Waveform; +import org.thunderdog.challegram.core.Background; +import org.thunderdog.challegram.data.TGRecord; +import org.thunderdog.challegram.theme.Theme; +import org.thunderdog.challegram.tool.Fonts; +import org.thunderdog.challegram.tool.Screen; +import org.thunderdog.challegram.tool.Strings; +import org.thunderdog.challegram.tool.UI; + +import me.vkryl.android.animator.FactorAnimator; + +public class VoiceWaveformView extends View implements FactorAnimator.Target { + private final Paint textPaint; + private final int textOffset, textRight; + private final int waveLeft; + + private final Waveform waveform; + + public VoiceWaveformView (Context context) { + super(context); + + textPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); + textPaint.setTypeface(Fonts.getRobotoRegular()); + textPaint.setTextSize(Screen.dp(15f)); + textOffset = Screen.dp(5f); + textRight = Screen.dp(39f); + waveLeft = Screen.dp(10f); + + waveform = new Waveform(null, Waveform.MODE_RECT, false); + } + + public void clearData () { + waveform.setData(null); + invalidate(); + } + + private TGRecord record; + private String seekStr; + + public void processRecord (final TGRecord record) { + this.record = record; + this.seekStr = Strings.buildDuration(record.getDuration()); + + Background.instance().post(() -> { + final byte[] waveform = record.getWaveform() != null ? record.getWaveform() : N.getWaveform(record.getPath()); + if (waveform != null) { + UI.post(() -> setWaveform(record, waveform)); + } + }); + } + + private final FactorAnimator waveformAnimator = new FactorAnimator(0, this, overshoot, OPEN_DURATION); + + private void setWaveform (final TGRecord record, byte[] waveform) { + if (this.record == null || !this.record.equals(record)) { + return; + } + record.setWaveform(waveform); + this.waveform.setData(waveform); + waveformAnimator.forceFactor(0f); + waveformAnimator.setStartDelay(80L); + waveformAnimator.animateTo(1f); + invalidate(); + } + + @Override + public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { + waveform.setExpand(factor); + invalidate(); + } + + + private int calculateWaveformWidth () { + return getMeasuredWidth() - waveLeft - Screen.dp(110f) + Screen.dp(55f); + } + + @Override + protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (getMeasuredWidth() != 0) { + waveform.layout(calculateWaveformWidth()); + } + } + + @Override + protected void onDraw (@NonNull Canvas c) { + int width = getMeasuredWidth(); + int height = getMeasuredHeight(); + int centerY = (int) ((float) height * .5f); + + if (seekStr != null) { + textPaint.setColor(Theme.textAccentColor()); + c.drawText(seekStr, width - textRight, centerY + textOffset, textPaint); + } + + waveform.draw(c, 1f, waveLeft, centerY); + } + + private static final AnticipateOvershootInterpolator overshoot = new AnticipateOvershootInterpolator(3.0f); + + private static final long OPEN_DURATION = 350L; +} diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/RandomAccessDataSource.java b/app/src/main/java/org/thunderdog/challegram/telegram/RandomAccessDataSource.java new file mode 100644 index 0000000000..c149e4cfac --- /dev/null +++ b/app/src/main/java/org/thunderdog/challegram/telegram/RandomAccessDataSource.java @@ -0,0 +1,105 @@ +/* + * This file is a part of Telegram X + * Copyright © 2014 (tgx-android@pm.me) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * File created on 30/06/2024 + */ +package org.thunderdog.challegram.telegram; + +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.datasource.BaseDataSource; +import androidx.media3.datasource.DataSource; +import androidx.media3.datasource.DataSpec; + +import java.io.IOException; +import java.io.RandomAccessFile; + + +public class RandomAccessDataSource extends BaseDataSource { + + public static final class Factory implements DataSource.Factory { + private final RandomAccessFile file; + + public Factory(RandomAccessFile file) { + this.file = file; + } + + @Override + @NonNull + public DataSource createDataSource() { + return new RandomAccessDataSource(file); + } + } + + private final RandomAccessFile file; + private long bytesRemaining; + private boolean opened; + + protected RandomAccessDataSource(RandomAccessFile file) { + super(false); + this.file = file; + } + + @Override + public long open(@NonNull DataSpec dataSpec) throws IOException { + transferInitializing(dataSpec); + + file.seek(dataSpec.position); + bytesRemaining = file.length() - dataSpec.position; + + if (bytesRemaining < 0) { + throw new IOException("Unsufficient length for reading."); + } + + opened = true; + transferStarted(dataSpec); + return bytesRemaining; + } + + @Nullable + @Override + public Uri getUri() { + return Uri.EMPTY; + } + + @Override + public int read(@NonNull byte[] buffer, int offset, int length) throws IOException { + if (length == 0) { + return 0; + } else if (bytesRemaining == 0) { + return C.RESULT_END_OF_INPUT; + } + + int bytesToRead = (int) Math.min(bytesRemaining, length); + int bytesRead = file.read(buffer, offset, bytesToRead); + + if (bytesRead == -1) { + return C.RESULT_END_OF_INPUT; + } + + bytesRemaining -= bytesRead; + bytesTransferred(bytesRead); + + return bytesRead; + } + + @Override + public void close() throws IOException { + if (opened) { + opened = false; + transferEnded(); + } + } +} diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/Tdlib.java b/app/src/main/java/org/thunderdog/challegram/telegram/Tdlib.java index b1d72fd089..1c0d6e452f 100644 --- a/app/src/main/java/org/thunderdog/challegram/telegram/Tdlib.java +++ b/app/src/main/java/org/thunderdog/challegram/telegram/Tdlib.java @@ -1908,7 +1908,7 @@ public static class Generation { public TdApi.File file; public String destinationPath; - private long generationId; + public long generationId; private boolean isPending; public Runnable onCancel; @@ -1916,6 +1916,7 @@ public static class Generation { private final HashMap awaitingGenerations = new HashMap<>(); private final HashMap pendingGenerations = new HashMap<>(); + private final HashMap fileWaitingGenerations = new HashMap<>(); public @Nullable Generation generateFile (String id, TdApi.FileType fileType, boolean isSecret, int priority, long timeoutMs) { final CountDownLatch latch = new CountDownLatch(2); @@ -1964,10 +1965,34 @@ public static class Generation { public void finishGeneration (Generation generation, @Nullable TdApi.Error error) { synchronized (awaitingGenerations) { pendingGenerations.remove(generation.generationId); + if (error == null && generation.file != null) { + fileWaitingGenerations.put(generation.file.id, generation); + files().subscribe(generation.file.id, obtainGeneratedFilesListener()); + } } client().send(new TdApi.FinishFileGeneration(generation.generationId, error), silentHandler()); } + private TdlibFilesManager.SimpleListener generatedFilesListener; + + private TdlibFilesManager.SimpleListener obtainGeneratedFilesListener () { + if (generatedFilesListener == null) { + generatedFilesListener = file -> { + if (!StringUtils.isEmpty(file.local.path) && file.size != 0 && file.local.isDownloadingCompleted) { + synchronized (awaitingGenerations) { + Generation generation = fileWaitingGenerations.remove(file.id); + files().unsubscribe(file.id, generatedFilesListener); + if (generation != null) { + generation.file = file; + } + } + } + }; + } + + return generatedFilesListener; + } + public void getMessage (long chatId, long messageId, @Nullable RunnableData callback) { client().send(new TdApi.GetMessageLocally(chatId, messageId), localResult -> { if (localResult instanceof TdApi.Message) { @@ -9924,6 +9949,12 @@ private void updateFileGenerationStop (TdApi.UpdateFileGenerationStop update) { synchronized (awaitingGenerations) { Generation generation = pendingGenerations.remove(update.generationId); if (generation != null) { + if (generation.file != null) { + fileWaitingGenerations.remove(generation.file.id); + if (generatedFilesListener != null) { + files().unsubscribe(generation.file.id, generatedFilesListener); + } + } if (generation.onCancel != null) { generation.onCancel.run(); } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/MessagesController.java b/app/src/main/java/org/thunderdog/challegram/ui/MessagesController.java index a0a39645de..47cd49536d 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/MessagesController.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/MessagesController.java @@ -118,7 +118,6 @@ import org.thunderdog.challegram.component.chat.StickerSuggestionAdapter; import org.thunderdog.challegram.component.chat.TdlibSingleUnreadReactionsManager; import org.thunderdog.challegram.component.chat.TopBarView; -import org.thunderdog.challegram.component.chat.VoiceInputView; import org.thunderdog.challegram.component.chat.VoiceVideoButtonView; import org.thunderdog.challegram.component.chat.WallpaperAdapter; import org.thunderdog.challegram.component.chat.WallpaperRecyclerView; @@ -145,7 +144,6 @@ import org.thunderdog.challegram.data.TGMessageLocation; import org.thunderdog.challegram.data.TGMessageMedia; import org.thunderdog.challegram.data.TGMessageSticker; -import org.thunderdog.challegram.data.TGRecord; import org.thunderdog.challegram.data.TGSwitchInline; import org.thunderdog.challegram.data.TGUser; import org.thunderdog.challegram.data.ThreadInfo; @@ -155,7 +153,6 @@ import org.thunderdog.challegram.helper.FoundUrls; import org.thunderdog.challegram.helper.LinkPreview; import org.thunderdog.challegram.helper.LiveLocationHelper; -import org.thunderdog.challegram.helper.Recorder; import org.thunderdog.challegram.loader.ImageFile; import org.thunderdog.challegram.loader.ImageGalleryFile; import org.thunderdog.challegram.loader.ImageReader; @@ -296,7 +293,7 @@ public class MessagesController extends ViewController implements Menu, Unlockable, View.OnClickListener, ActivityResultHandler, MoreDelegate, CommandKeyboardLayout.Callback, MediaCollectorDelegate, SelectDelegate, - ReplyBarView.Callback, RaiseHelper.Listener, VoiceInputView.Callback, + ReplyBarView.Callback, RaiseHelper.Listener, TGLegacyManager.EmojiLoadListener, ChatHeaderView.Callback, ChatListener, NotificationSettingsListener, EmojiLayout.Listener, MessageThreadListener, TdlibSingleUnreadReactionsManager.UnreadSingleReactionListener, @@ -433,14 +430,10 @@ public boolean onHapticMenuItemClick (View view, View parentView, HapticMenuHelp openSetSenderPopup(); } else if (viewId == R.id.btn_sendOnceOnline) { TdApi.MessageSendOptions sendOptions = Td.newSendOptions(new TdApi.MessageSchedulingStateSendWhenOnline()); - if (!sendShowingVoice(sendButton, sendOptions)) { - send(sendOptions, true); - } + send(sendOptions, true); } else if (viewId == R.id.btn_sendScheduled) { tdlib.ui().pickSchedulingState(this, sendOptions -> { - if (!sendShowingVoice(sendButton, sendOptions)) { - send(sendOptions, true); - } + send(sendOptions, true); }, getChatId(), false, false, null, null); } else if (viewId == R.id.btn_sendNoMarkdown) { if (isEditingMessage()) { @@ -454,9 +447,7 @@ public boolean onHapticMenuItemClick (View view, View parentView, HapticMenuHelp showCallbackToast(text); } else if (viewId == R.id.btn_sendNoSound) { pickDateOrProceed(Td.newSendOptions(true), (modifiedSendOptions, disableMarkdown) -> { - if (!sendShowingVoice(sendButton, modifiedSendOptions)) { - send(modifiedSendOptions, true); - } + send(modifiedSendOptions, true); }); } else if (viewId == R.id.btn_debugLtrEmoji) { pickDateOrProceed(Td.newSendOptions(), (sendOptions, disableMarkdown) -> send(new TdApi.InputMessageText(new TdApi.FormattedText(Text.bidiGenerateTestMessage(), new TdApi.TextEntity[0]), null, false), false, sendOptions, null)); @@ -2049,73 +2040,71 @@ private void send (TdApi.MessageSendOptions sendOptions) { } private void send (TdApi.MessageSendOptions sendOptions, boolean applyMarkdown) { - if (!sendShowingVoice(sendButton, sendOptions)) { - if (isEditingMessage()) { - saveMessage(applyMarkdown); - } else if (hasAttachedFiles()) { - if (isSendingText) { - return; - } + if (isEditingMessage()) { + saveMessage(applyMarkdown); + } else if (hasAttachedFiles()) { + if (isSendingText) { + return; + } - final TdApi.FormattedText caption = inputView != null ? inputView.getOutputText(applyMarkdown) : null; - final ArrayList> selectedItems = attachedFiles.getCurrentItems(); + final TdApi.FormattedText caption = inputView != null ? inputView.getOutputText(applyMarkdown) : null; + final ArrayList> selectedItems = attachedFiles.getCurrentItems(); - final ArrayList musicEntries = new ArrayList<>(); - final ArrayList files = new ArrayList<>(); + final ArrayList musicEntries = new ArrayList<>(); + final ArrayList files = new ArrayList<>(); - for (InlineResult result : selectedItems) { - switch (result.getType()) { - case InlineResult.TYPE_AUDIO: { - musicEntries.add((MediaBottomFilesController.MusicEntry) ((InlineResultCommon) result).getTag()); - break; - } - case InlineResult.TYPE_DOCUMENT: { - files.add(result.getId()); - break; - } + for (InlineResult result : selectedItems) { + switch (result.getType()) { + case InlineResult.TYPE_AUDIO: { + musicEntries.add((MediaBottomFilesController.MusicEntry) ((InlineResultCommon) result).getTag()); + break; + } + case InlineResult.TYPE_DOCUMENT: { + files.add(result.getId()); + break; } } + } - if (musicEntries.isEmpty() && files.isEmpty()) { - return; - } + if (musicEntries.isEmpty() && files.isEmpty()) { + return; + } - final List sentMessages = new ArrayList<>(selectedItems.size()); - final TdApi.InputMessageReplyTo replyTo = getCurrentReplyId(); - final ArrayList> functions = new ArrayList<>(); - final boolean[] isTimeout = new boolean[1]; - final long chatId = getChatId(); + final List sentMessages = new ArrayList<>(selectedItems.size()); + final TdApi.InputMessageReplyTo replyTo = getCurrentReplyId(); + final ArrayList> functions = new ArrayList<>(); + final boolean[] isTimeout = new boolean[1]; + final long chatId = getChatId(); - setIsSendingText(true); - manager.setSentMessages(sentMessages); - Runnable clearInputRunnable = () -> { - if (getChatId() == chatId) { - clearInputAfterSend(true, true, replyTo, true); - UI.showToast(Lang.getString(R.string.SlowFileAccess), Toast.LENGTH_LONG); - isTimeout[0] = true; - } - }; + setIsSendingText(true); + manager.setSentMessages(sentMessages); + Runnable clearInputRunnable = () -> { + if (getChatId() == chatId) { + clearInputAfterSend(true, true, replyTo, true); + UI.showToast(Lang.getString(R.string.SlowFileAccess), Toast.LENGTH_LONG); + isTimeout[0] = true; + } + }; - UI.post(clearInputRunnable, MathUtils.clamp(50 * selectedItems.size(), 200, 500)); + UI.post(clearInputRunnable, MathUtils.clamp(50 * selectedItems.size(), 200, 500)); - final List> musicFunctions = getSendMusicFunctions(sendButton, musicEntries, true, !musicEntries.isEmpty(), caption, sendOptions); - sendFiles(sendButton, files, true, true, !musicEntries.isEmpty() && !files.isEmpty() ? null : caption, sendOptions, filesFunctions -> { - if (filesFunctions != null) { - functions.addAll(filesFunctions); - } - if (musicFunctions != null) { - functions.addAll(musicFunctions); + final List> musicFunctions = getSendMusicFunctions(sendButton, musicEntries, true, !musicEntries.isEmpty(), caption, sendOptions); + sendFiles(sendButton, files, true, true, !musicEntries.isEmpty() && !files.isEmpty() ? null : caption, sendOptions, filesFunctions -> { + if (filesFunctions != null) { + functions.addAll(filesFunctions); + } + if (musicFunctions != null) { + functions.addAll(musicFunctions); + } + executeSendMessageFunctions(functions, sentMessages, sendOptions != null && sendOptions.schedulingState != null, success -> UI.post(() -> { + if (!isTimeout[0] && getChatId() == chatId) { + clearInputAfterSend(true, true, replyTo, true); } - executeSendMessageFunctions(functions, sentMessages, sendOptions != null && sendOptions.schedulingState != null, success -> UI.post(() -> { - if (!isTimeout[0] && getChatId() == chatId) { - clearInputAfterSend(true, true, replyTo, true); - } - UI.cancel(clearInputRunnable); - })); - }); - } else { - sendText(applyMarkdown, sendOptions); - } + UI.cancel(clearInputRunnable); + })); + }); + } else { + sendText(applyMarkdown, sendOptions); } } @@ -2712,14 +2701,6 @@ public void shareItem (Object item) { return; } - if (item instanceof TGRecord) { - if (!hasSendMessagePermission(RightId.SEND_VOICE_NOTES)) { - return; - } - processRecord((TGRecord) item); - return; - } - if (item instanceof TGBotStart) { TGBotStart start = (TGBotStart) item; @@ -3920,7 +3901,7 @@ public void onAccountSwitched (TdlibAccount newAccount, TdApi.User profile, int private static HashSet shownTutorials; private void showMessageMenuTutorial () { - if (sendShown.getValue() && !areScheduledOnly() && !isInputLess() && canWriteMessages() && hasSendBasicMessagePermission() && !isEditingMessage() && !isSecretChat() && isFocused() && !isVoicePreviewShowing() && !sendButton.inInlineMode()) { + if (sendShown.getValue() && !areScheduledOnly() && !isInputLess() && canWriteMessages() && hasSendBasicMessagePermission() && !isEditingMessage() && !isSecretChat() && isFocused() && !sendButton.inInlineMode()) { long tutorialFlag; if (isSelfChat()) { tutorialFlag = Settings.TUTORIAL_SET_REMINDER; @@ -4339,7 +4320,6 @@ public void destroy () { // messagesView.clear(); - closeVoicePreview(true); closeEmojiKeyboard(); clearScheduledKeyboard(); @@ -8644,10 +8624,6 @@ public boolean onBackPressed (boolean fromTop) { context.getRecordAudioVideoController().finishRecording(true); return true; } - if (isVoiceShowing) { - onDiscardVoiceRecord(); - return true; - } if (hasEditedChanges()) { if (isEditingCaption()) { showUnsavedChangesPromptBeforeLeaving(Lang.getString(R.string.DiscardEditCaptionHint), Lang.getString(R.string.DiscardEditCaption), null); @@ -9441,7 +9417,7 @@ public void updateSendButton (CharSequence message, boolean animated) { } private void checkSendButton (boolean animated) { - setSendVisible(inputView.getText().length() > 0 || isEditingMessage() || hasAttachedFiles() || isVoiceShowing, animated && getParentOrSelf().isAttachedToNavigationController()); + setSendVisible(inputView.getText().length() > 0 || isEditingMessage() || hasAttachedFiles(), animated && getParentOrSelf().isAttachedToNavigationController()); } private void displaySendButton () { @@ -9822,24 +9798,6 @@ private List> getSendMusicFunctions (View view, List { - byte[] waveform = N.getWaveform(record.getPath()); - tdlib.sendMessage(chatId, getMessageThreadId(), replyTo, finalSendOptions, new TdApi.InputMessageVoiceNote(record.toInputFile(), record.getDuration(), waveform, null, null), null); - }); - } else { - tdlib.sendMessage(chatId, getMessageThreadId(), replyTo, finalSendOptions, new TdApi.InputMessageVoiceNote(record.toInputFile(), record.getDuration(), record.getWaveform(), null, null), null); - } - return true; - } - public void forwardMessage (TdApi.Message message) { // TODO remove all related to Forward stuff to replace with ShareLayout if (tdlib.getRestrictionText(chat, message) == null) { tdlib.forwardMessage(chat.id, getMessageThreadId(), message.chatId, message.id, Td.newSendOptions(obtainSilentMode())); @@ -10929,100 +10887,6 @@ public boolean leaveRaiseMode () { return true; } - public boolean isVoicePreviewShowing () { - return isVoiceShowing; - } - - private boolean isVoiceShowing, isVoiceReceived, ignoreVoice; - private VoiceInputView voiceInputView; - - public void prepareVoicePreview (int seconds) { - if (voiceInputView == null) { - voiceInputView = new VoiceInputView(context()); - voiceInputView.addThemeListeners(this); - voiceInputView.setCallback(this); - contentView.addView(voiceInputView); - } else { - voiceInputView.clearData(); - voiceInputView.cancelCloseAnimation(); - voiceInputView.setVisibility(View.VISIBLE); - voiceInputView.setAlpha(1f); - } - - hideSoftwareKeyboard(); - closeCommandsKeyboard(false); - closeEmojiKeyboard(); - - voiceInputView.setDuration(seconds); - - isVoiceReceived = false; - isVoiceShowing = true; - - checkSendButton(false); - } - - private boolean sendShowingVoice (View view, TdApi.MessageSendOptions sendOptions) { - if (!isVoiceShowing) { - return false; - } - if (showSlowModeRestriction(view, sendOptions) || showRestriction(view, RightId.SEND_VOICE_NOTES)) { - return false; - } - TGRecord record = voiceInputView.getRecord(); - if (record != null) { - voiceInputView.ignoreStop(); - // Player.instance().destroy(); - if (sendRecord(view, record, true, sendOptions)) { - closeVoicePreview(false); - } - } - return true; - } - - public void forceCloseVoicePreview () { - closeVoicePreview(true); - } - - private void closeVoicePreview (boolean force) { - if (!isVoiceShowing) { - return; - } - isVoiceShowing = false; - checkSendButton(!force); - if (force) { - voiceInputView.discardRecord(); - voiceInputView.setAlpha(0f); - voiceInputView.setVisibility(View.GONE); - } else { - voiceInputView.animateClose(); - } - } - - @Override - public void onDiscardVoiceRecord () { - if (isVoiceShowing) { - if (!isVoiceReceived) { - ignoreVoice = true; - } - voiceInputView.ignoreStop(); - // Player.instance().destroy(); - voiceInputView.discardRecord(); - closeVoicePreview(false); - } - } - - public void processRecord (TGRecord record) { - if (ignoreVoice) { - Recorder.instance().delete(record); - ignoreVoice = false; - } else if (isVoiceShowing) { - isVoiceReceived = true; - voiceInputView.processRecord(record); - } else { - sendRecord(sendButton, record, true, Td.newSendOptions()); - } - } - // Messages search private FrameLayoutFix searchControlsLayout; diff --git a/app/src/main/java/org/thunderdog/challegram/ui/camera/legacy/CameraApi.java b/app/src/main/java/org/thunderdog/challegram/ui/camera/legacy/CameraApi.java index 186b254cdb..40510e447c 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/camera/legacy/CameraApi.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/camera/legacy/CameraApi.java @@ -146,10 +146,15 @@ public final void switchToNextCameraDevice () { } return; } - if (isCameraActive && cameraOpened) { - if (roundRecorder != null && !roundRecorder.canSwitchToNewCamera()) { - return; + + if (roundRecorder != null) { + if (roundRecorder.canSwitchToNewCamera()) { + onNextCameraSourceRequested(); } + return; + } + + if (isCameraActive && cameraOpened) { onNextCameraSourceRequested(); } } diff --git a/app/src/main/java/org/thunderdog/challegram/ui/camera/legacy/CameraApiLegacy.java b/app/src/main/java/org/thunderdog/challegram/ui/camera/legacy/CameraApiLegacy.java index 1b4eb176ba..7fda8665f6 100644 --- a/app/src/main/java/org/thunderdog/challegram/ui/camera/legacy/CameraApiLegacy.java +++ b/app/src/main/java/org/thunderdog/challegram/ui/camera/legacy/CameraApiLegacy.java @@ -69,7 +69,8 @@ private void resetContextualSettings () { @Override protected void onNextCameraSourceRequested () { - if (isCameraActive && mNumberOfCameras > 1) { + final boolean isActive = isCameraActive; + if (mNumberOfCameras > 1) { resetContextualSettings(); manager.resetRenderState(true); int nextCameraIndex = getNextCameraIndex(); @@ -77,9 +78,13 @@ protected void onNextCameraSourceRequested () { boolean forward = nextCameraIndex >= getRequestedCameraIndex(); boolean toFrontFace = nextCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT; manager.onCameraSourceChange(false, forward, toFrontFace); - setCameraActive(false); + if (isActive) { + setCameraActive(false); + } setRequestedCameraIndex(nextCameraIndex); - setCameraActive(true); + if (isActive) { + setCameraActive(true); + } manager.onCameraSourceChange(true, forward, toFrontFace); } } diff --git a/app/src/main/java/org/thunderdog/challegram/video/old/Track.java b/app/src/main/java/org/thunderdog/challegram/video/old/Track.java index 6e71ed157a..31de718a4a 100644 --- a/app/src/main/java/org/thunderdog/challegram/video/old/Track.java +++ b/app/src/main/java/org/thunderdog/challegram/video/old/Track.java @@ -271,6 +271,8 @@ public void addSample(long offset, MediaCodec.BufferInfo bufferInfo) { } public void prepare() { + duration = 0; + ArrayList original = new ArrayList<>(samplePresentationTimes); Collections.sort(samplePresentationTimes, (o1, o2) -> { if (o1.presentationTime > o2.presentationTime) { diff --git a/app/src/main/java/org/thunderdog/challegram/widget/FileProgressComponent.java b/app/src/main/java/org/thunderdog/challegram/widget/FileProgressComponent.java index 69ec549cee..05a7b6e936 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/FileProgressComponent.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/FileProgressComponent.java @@ -108,6 +108,9 @@ public void handleMessage (Message msg) { public static final @DrawableRes int PLAY_ICON = R.drawable.baseline_play_arrow_36_white; public interface SimpleListener { + default boolean onPlayPauseClick (FileProgressComponent context, View view, TdApi.File file, long messageId) { + return false; + } default boolean onClick (FileProgressComponent context, View view, TdApi.File file, long messageId) { return false; } @@ -138,6 +141,7 @@ default void onProgress (TdApi.File file, float progress) { } private boolean isLocal; private boolean ignoreLoaderClicks; + private boolean ignorePlayPauseClicks; private boolean noCloud; private final Rect vsDownloadRect = new Rect(); @@ -189,6 +193,10 @@ public void setIgnoreLoaderClicks (boolean ignoreLoaderClicks) { this.ignoreLoaderClicks = ignoreLoaderClicks; } + public void setIgnorePlayPauseClicks (boolean ignorePlayPauseClicks) { + this.ignorePlayPauseClicks = ignorePlayPauseClicks; + } + public void setVideoStreaming (boolean isVideoStreaming) { boolean isUpdated = this.isVideoStreaming != isVideoStreaming; this.isVideoStreaming = isVideoStreaming; @@ -770,7 +778,13 @@ public boolean performClick (View view, boolean ignoreListener) { TGDownloadManager.instance().downloadFile(file); }*/ if (file.remote.isUploadingCompleted || file.id == -1) { - TdlibManager.instance().player().playPauseMessage(tdlib, playPauseFile, playListBuilder); + if (ignorePlayPauseClicks) { + if (listener != null) { + listener.onPlayPauseClick(this, view, file, messageId); + } + } else { + TdlibManager.instance().player().playPauseMessage(tdlib, playPauseFile, playListBuilder); + } } return true; } diff --git a/app/src/main/java/org/thunderdog/challegram/widget/VideoTimelineView.java b/app/src/main/java/org/thunderdog/challegram/widget/VideoTimelineView.java index fade831501..12d269d5e3 100644 --- a/app/src/main/java/org/thunderdog/challegram/widget/VideoTimelineView.java +++ b/app/src/main/java/org/thunderdog/challegram/widget/VideoTimelineView.java @@ -236,6 +236,7 @@ public interface TimelineDelegate { void onTrimStartEnd (VideoTimelineView v, boolean isStarted); default void onVideoLoaded (VideoTimelineView v, double totalDuration, double width, double height, int frameRate, long bitrate) { } void onTimelineTrimChanged (VideoTimelineView v, double totalDuration, double startTimeSeconds, double endTimeSeconds); + default void onTimelineVisualTrimChanged (VideoTimelineView v, double totalDuration, double startTimeSeconds, double endTimeSeconds) {} void onSeekTo (VideoTimelineView v, float progress); } @@ -337,6 +338,27 @@ private TooltipOverlayView.LocationProvider locationProvider (int mode) { return null; } + public void performSliderDown (boolean isEnd) { + setMoving(true, true); + setSlideMode(isEnd ? SLIDE_MODE_END : SLIDE_MODE_START); + showTooltip(); + } + + public void performSliderMove (float factor, boolean isEnd) { + final var animator = isEnd ? endFactor : startFactor; + if (animator.getFactor() != factor) { + animator.forceFactor(factor); + updateTooltip(isEnd); + invalidate(); + } + } + + public void performSliderUp (boolean isEnd) { + setSlideMode(SLIDE_MODE_NONE); + setMoving(false, true); + normalizeValues(isEnd); + } + @Override public boolean onTouchEvent (MotionEvent e) { float x = e.getX(); @@ -578,6 +600,9 @@ private void addFrame (int frameCount, Frame frame) { public void onFactorChanged (int id, float factor, float fraction, FactorAnimator callee) { updateTooltip(id == 1); invalidate(); + if (delegate != null) { + delegate.onTimelineVisualTrimChanged(this, totalDuration, getCurrentStart(), getCurrentEnd()); + } } @Override diff --git a/app/src/main/other/themes/Night Black.tgx-theme b/app/src/main/other/themes/Night Black.tgx-theme index 2f795d08a1..fe502ee7de 100644 --- a/app/src/main/other/themes/Night Black.tgx-theme +++ b/app/src/main/other/themes/Night Black.tgx-theme @@ -68,4 +68,7 @@ avatarOrange, avatarOrange_big, nameOrange: #D67722 avatarPink, avatarPink_big, namePink: #C7508B avatarRed, avatarRed_big, nameRed: #CC5049 avatarViolet, avatarViolet_big: #955CDB -avatarYellow, avatarYellow_big, nameYellow: #F9C84A \ No newline at end of file +avatarYellow, avatarYellow_big, nameYellow: #F9C84A +blockQuoteText, blockQuoteLine: #A0A0A0 +bubbleIn_blockQuoteText, bubbleIn_blockQuoteLine: #A0A0A0 +bubbleOut_blockQuoteText, bubbleOut_blockQuoteLine: #A0A0A0 diff --git a/app/src/main/other/themes/colors-and-properties.xml b/app/src/main/other/themes/colors-and-properties.xml index d21a7c1173..795c154741 100644 --- a/app/src/main/other/themes/colors-and-properties.xml +++ b/app/src/main/other/themes/colors-and-properties.xml @@ -69,7 +69,7 @@ - + @@ -563,11 +563,11 @@ - - - - - - + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_hot_once_24.xml b/app/src/main/res/drawable/baseline_hot_once_24.xml new file mode 100644 index 0000000000..b6faed8cdc --- /dev/null +++ b/app/src/main/res/drawable/baseline_hot_once_24.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2df667417f..df11eef246 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2297,6 +2297,8 @@ Record HQ Round Videos Discard Video Message Are you sure you want to discard your video message? + Discard Audio Message + Are you sure you want to discard your audio message? Discard Feature is not available for this type of media %1$s made group history hidden for new members @@ -4112,6 +4114,9 @@ Set a Reminder Save without markdown + Please, turn on the sound first. + Wait until the file is completely downloaded. + Send in %1$s minute Send in %1$s minutes Send in %1$s hour