Show remaining time on wave form view and cache wave form in database.
This commit is contained in:
parent
e01838e996
commit
3fec23fd36
33 changed files with 357 additions and 162 deletions
|
@ -1,9 +1,11 @@
|
|||
package org.thoughtcrime.securesms.attachments;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.audio.AudioHash;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
|
||||
|
@ -51,6 +53,9 @@ public abstract class Attachment {
|
|||
@Nullable
|
||||
private final BlurHash blurHash;
|
||||
|
||||
@Nullable
|
||||
private final AudioHash audioHash;
|
||||
|
||||
@NonNull
|
||||
private final TransformProperties transformProperties;
|
||||
|
||||
|
@ -58,7 +63,7 @@ public abstract class Attachment {
|
|||
int cdnNumber, @Nullable String location, @Nullable String key, @Nullable String relay,
|
||||
@Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote,
|
||||
int width, int height, boolean quote, long uploadTimestamp, @Nullable String caption,
|
||||
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash,
|
||||
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable AudioHash audioHash,
|
||||
@Nullable TransformProperties transformProperties)
|
||||
{
|
||||
this.contentType = contentType;
|
||||
|
@ -79,6 +84,7 @@ public abstract class Attachment {
|
|||
this.stickerLocator = stickerLocator;
|
||||
this.caption = caption;
|
||||
this.blurHash = blurHash;
|
||||
this.audioHash = audioHash;
|
||||
this.transformProperties = transformProperties != null ? transformProperties : TransformProperties.empty();
|
||||
}
|
||||
|
||||
|
@ -172,6 +178,10 @@ public abstract class Attachment {
|
|||
return blurHash;
|
||||
}
|
||||
|
||||
public @Nullable AudioHash getAudioHash() {
|
||||
return audioHash;
|
||||
}
|
||||
|
||||
public @Nullable String getCaption() {
|
||||
return caption;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.net.Uri;
|
|||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.audio.AudioHash;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
|
@ -25,11 +26,11 @@ public class DatabaseAttachment extends Attachment {
|
|||
String fileName, int cdnNumber, String location, String key, String relay,
|
||||
byte[] digest, String fastPreflightId, boolean voiceNote,
|
||||
int width, int height, boolean quote, @Nullable String caption,
|
||||
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash,
|
||||
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable AudioHash audioHash,
|
||||
@Nullable TransformProperties transformProperties, int displayOrder,
|
||||
long uploadTimestamp)
|
||||
{
|
||||
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, transformProperties);
|
||||
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||
this.attachmentId = attachmentId;
|
||||
this.hasData = hasData;
|
||||
this.hasThumbnail = hasThumbnail;
|
||||
|
|
|
@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase;
|
|||
public class MmsNotificationAttachment extends Attachment {
|
||||
|
||||
public MmsNotificationAttachment(int status, long size) {
|
||||
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, 0, 0, false, 0, null, null, null, null);
|
||||
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, 0, 0, false, 0, null, null, null, null, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.net.Uri;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.audio.AudioHash;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
|
@ -24,7 +25,7 @@ public class PointerAttachment extends Attachment {
|
|||
int width, int height, long uploadTimestamp, @Nullable String caption, @Nullable StickerLocator stickerLocator,
|
||||
@Nullable BlurHash blurHash)
|
||||
{
|
||||
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null);
|
||||
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
|
|
@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
|||
public class TombstoneAttachment extends Attachment {
|
||||
|
||||
public TombstoneAttachment(@NonNull String contentType, boolean quote) {
|
||||
super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, 0, 0, quote, 0, null, null, null, null);
|
||||
super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, 0, 0, quote, 0, null, null, null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.net.Uri;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.audio.AudioHash;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
|
@ -16,18 +17,18 @@ public class UriAttachment extends Attachment {
|
|||
|
||||
public UriAttachment(@NonNull Uri uri, @NonNull String contentType, int transferState, long size,
|
||||
@Nullable String fileName, boolean voiceNote, boolean quote, @Nullable String caption,
|
||||
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable TransformProperties transformProperties)
|
||||
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable AudioHash audioHash, @Nullable TransformProperties transformProperties)
|
||||
{
|
||||
this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, quote, caption, stickerLocator, blurHash, transformProperties);
|
||||
this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||
}
|
||||
|
||||
public UriAttachment(@NonNull Uri dataUri, @Nullable Uri thumbnailUri,
|
||||
@NonNull String contentType, int transferState, long size, int width, int height,
|
||||
@Nullable String fileName, @Nullable String fastPreflightId,
|
||||
boolean voiceNote, boolean quote, @Nullable String caption, @Nullable StickerLocator stickerLocator,
|
||||
@Nullable BlurHash blurHash, @Nullable TransformProperties transformProperties)
|
||||
@Nullable BlurHash blurHash, @Nullable AudioHash audioHash, @Nullable TransformProperties transformProperties)
|
||||
{
|
||||
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, width, height, quote, 0, caption, stickerLocator, blurHash, transformProperties);
|
||||
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
|
||||
this.dataUri = dataUri;
|
||||
this.thumbnailUri = thumbnailUri;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
package org.thoughtcrime.securesms.audio;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData;
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* An AudioHash is a compact string representation of the wave form and duration for an audio file.
|
||||
*/
|
||||
public final class AudioHash {
|
||||
|
||||
@NonNull private final String hash;
|
||||
@NonNull private final AudioWaveFormData audioWaveForm;
|
||||
|
||||
private AudioHash(@NonNull String hash, @NonNull AudioWaveFormData audioWaveForm) {
|
||||
this.hash = hash;
|
||||
this.audioWaveForm = audioWaveForm;
|
||||
}
|
||||
|
||||
public AudioHash(@NonNull AudioWaveFormData audioWaveForm) {
|
||||
this(Base64.encodeBytes(audioWaveForm.toByteArray()), audioWaveForm);
|
||||
}
|
||||
|
||||
public static @Nullable AudioHash parseOrNull(@Nullable String hash) {
|
||||
if (hash == null) return null;
|
||||
try {
|
||||
return new AudioHash(hash, AudioWaveFormData.parseFrom(Base64.decode(hash)));
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull AudioWaveFormData getAudioWaveForm() {
|
||||
return audioWaveForm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
AudioHash other = (AudioHash) o;
|
||||
return hash.equals(other.hash);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hash.hashCode();
|
||||
}
|
||||
|
||||
public @NonNull String getHash() {
|
||||
return hash;
|
||||
}
|
||||
}
|
|
@ -14,11 +14,18 @@ import androidx.annotation.RequiresApi;
|
|||
import androidx.annotation.WorkerThread;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.media.DecryptableUriMediaInput;
|
||||
import org.thoughtcrime.securesms.media.MediaInput;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SerialExecutor;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -32,7 +39,7 @@ public final class AudioWaveForm {
|
|||
|
||||
private static final String TAG = Log.tag(AudioWaveForm.class);
|
||||
|
||||
private static final int BARS = 46;
|
||||
private static final int BAR_COUNT = 46;
|
||||
private static final int SAMPLES_PER_BAR = 4;
|
||||
|
||||
private final Context context;
|
||||
|
@ -43,34 +50,68 @@ public final class AudioWaveForm {
|
|||
this.slide = slide;
|
||||
}
|
||||
|
||||
private static final LruCache<Uri, AudioFileInfo> WAVE_FORM_CACHE = new LruCache<>(200);
|
||||
private static final Executor AUDIO_DECODER_EXECUTOR = SignalExecutors.BOUNDED;
|
||||
private static final LruCache<String, AudioFileInfo> WAVE_FORM_CACHE = new LruCache<>(200);
|
||||
private static final Executor AUDIO_DECODER_EXECUTOR = new SerialExecutor(SignalExecutors.BOUNDED);
|
||||
|
||||
@AnyThread
|
||||
public void generateWaveForm(@NonNull Consumer<AudioFileInfo> onSuccess, @NonNull Consumer<IOException> onFailure) {
|
||||
public void getWaveForm(@NonNull Consumer<AudioFileInfo> onSuccess, @NonNull Consumer<IOException> onFailure) {
|
||||
Uri uri = slide.getUri();
|
||||
Attachment attachment = slide.asAttachment();
|
||||
|
||||
if (uri == null) {
|
||||
Log.w(TAG, "No uri");
|
||||
Util.runOnMain(() -> onFailure.accept(null));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(attachment instanceof DatabaseAttachment)) {
|
||||
Log.i(TAG, "Not yet in database");
|
||||
Util.runOnMain(() -> onFailure.accept(null));
|
||||
return;
|
||||
}
|
||||
|
||||
String cacheKey = uri.toString();
|
||||
AudioFileInfo cached = WAVE_FORM_CACHE.get(cacheKey);
|
||||
if (cached != null) {
|
||||
Log.i(TAG, "Loaded wave form from cache " + cacheKey);
|
||||
Util.runOnMain(() -> onSuccess.accept(cached));
|
||||
return;
|
||||
}
|
||||
|
||||
AUDIO_DECODER_EXECUTOR.execute(() -> {
|
||||
AudioFileInfo cachedInExecutor = WAVE_FORM_CACHE.get(cacheKey);
|
||||
if (cachedInExecutor != null) {
|
||||
Log.i(TAG, "Loaded wave form from cache inside executor" + cacheKey);
|
||||
Util.runOnMain(() -> onSuccess.accept(cachedInExecutor));
|
||||
return;
|
||||
}
|
||||
|
||||
AudioHash audioHash = attachment.getAudioHash();
|
||||
if (audioHash != null) {
|
||||
AudioFileInfo audioFileInfo = AudioFileInfo.fromDatabaseProtobuf(audioHash.getAudioWaveForm());
|
||||
if (audioFileInfo.waveForm.length != BAR_COUNT) {
|
||||
Log.w(TAG, "Wave form from database does not match bar count, regenerating " + cacheKey);
|
||||
} else {
|
||||
WAVE_FORM_CACHE.put(cacheKey, audioFileInfo);
|
||||
Log.i(TAG, "Loaded wave form from DB " + cacheKey);
|
||||
Util.runOnMain(() -> onSuccess.accept(audioFileInfo));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
long startTime = System.currentTimeMillis();
|
||||
Uri uri = slide.getUri();
|
||||
if (uri == null) {
|
||||
Util.runOnMain(() -> onFailure.accept(null));
|
||||
return;
|
||||
}
|
||||
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
|
||||
long startTime = System.currentTimeMillis();
|
||||
AudioFileInfo fileInfo = generateWaveForm(uri);
|
||||
|
||||
AudioFileInfo cached = WAVE_FORM_CACHE.get(uri);
|
||||
if (cached != null) {
|
||||
Util.runOnMain(() -> onSuccess.accept(cached));
|
||||
return;
|
||||
}
|
||||
Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey));
|
||||
|
||||
AudioFileInfo fileInfo = generateWaveForm(uri);
|
||||
WAVE_FORM_CACHE.put(uri, fileInfo);
|
||||
|
||||
Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms", System.currentTimeMillis() - startTime));
|
||||
DatabaseFactory.getAttachmentDatabase(context).writeAudioHash(dbAttachment.getAttachmentId(), fileInfo.toDatabaseProtobuf());
|
||||
|
||||
WAVE_FORM_CACHE.put(cacheKey, fileInfo);
|
||||
Util.runOnMain(() -> onSuccess.accept(fileInfo));
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "", e);
|
||||
Log.w(TAG, "Failed to create audio wave form for " + cacheKey, e);
|
||||
onFailure.accept(e);
|
||||
}
|
||||
});
|
||||
|
@ -83,17 +124,36 @@ public final class AudioWaveForm {
|
|||
*/
|
||||
@WorkerThread
|
||||
@RequiresApi(api = 23)
|
||||
private AudioFileInfo generateWaveForm(@NonNull Uri uri) throws IOException {
|
||||
private @NonNull AudioFileInfo generateWaveForm(@NonNull Uri uri) throws IOException {
|
||||
try (MediaInput dataSource = DecryptableUriMediaInput.createForUri(context, uri)) {
|
||||
long[] wave = new long[BARS];
|
||||
int[] waveSamples = new int[BARS];
|
||||
int[] inputSamples = new int[BARS * SAMPLES_PER_BAR];
|
||||
long[] wave = new long[BAR_COUNT];
|
||||
int[] waveSamples = new int[BAR_COUNT];
|
||||
int[] inputSamples = new int[BAR_COUNT * SAMPLES_PER_BAR];
|
||||
|
||||
MediaExtractor extractor = dataSource.createExtractor();
|
||||
MediaFormat format = extractor.getTrackFormat(0);
|
||||
long totalDurationUs = format.getLong(MediaFormat.KEY_DURATION);
|
||||
String mime = requireAudio(format.getString(MediaFormat.KEY_MIME));
|
||||
MediaCodec codec = MediaCodec.createDecoderByType(mime);
|
||||
MediaExtractor extractor = dataSource.createExtractor();
|
||||
|
||||
if (extractor.getTrackCount() == 0) {
|
||||
throw new IOException("No audio track");
|
||||
}
|
||||
|
||||
MediaFormat format = extractor.getTrackFormat(0);
|
||||
|
||||
if (!format.containsKey(MediaFormat.KEY_DURATION)) {
|
||||
throw new IOException("Unknown duration");
|
||||
}
|
||||
|
||||
long totalDurationUs = format.getLong(MediaFormat.KEY_DURATION);
|
||||
String mime = format.getString(MediaFormat.KEY_MIME);
|
||||
|
||||
if (!mime.startsWith("audio/")) {
|
||||
throw new IOException("Mime not audio");
|
||||
}
|
||||
|
||||
MediaCodec codec = MediaCodec.createDecoderByType(mime);
|
||||
|
||||
if (totalDurationUs == 0) {
|
||||
throw new IOException("Zero duration");
|
||||
}
|
||||
|
||||
codec.configure(format, null, null, 0);
|
||||
codec.start();
|
||||
|
@ -158,29 +218,24 @@ public final class AudioWaveForm {
|
|||
}
|
||||
|
||||
ByteBuffer buf = codecOutputBuffers[outputBufferIndex];
|
||||
int barIndex = (int) ((wave.length * info.presentationTimeUs) / totalDurationUs) - 1;
|
||||
int barIndex = (int) ((wave.length * info.presentationTimeUs) / totalDurationUs);
|
||||
long total = 0;
|
||||
for (int i = 0; i < info.size; i += 2 * 4) {
|
||||
short aShort = buf.getShort(i);
|
||||
total += Math.abs(aShort);
|
||||
}
|
||||
if (barIndex > 0) {
|
||||
if (barIndex >= 0 && barIndex < wave.length) {
|
||||
wave[barIndex] += total;
|
||||
waveSamples[barIndex] += info.size / 2;
|
||||
}
|
||||
codec.releaseOutputBuffer(outputBufferIndex, false);
|
||||
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
||||
Log.d(TAG, "saw output EOS.");
|
||||
sawOutputEOS = true;
|
||||
}
|
||||
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
|
||||
codecOutputBuffers = codec.getOutputBuffers();
|
||||
Log.d(TAG, "output buffers have changed.");
|
||||
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
||||
MediaFormat oformat = codec.getOutputFormat();
|
||||
Log.d(TAG, "output format has changed to " + oformat);
|
||||
} else {
|
||||
Log.d(TAG, "dequeueOutputBuffer returned " + outputBufferIndex);
|
||||
Log.d(TAG, "output format has changed to " + codec.getOutputFormat());
|
||||
}
|
||||
} while (outputBufferIndex >= 0);
|
||||
}
|
||||
|
@ -189,36 +244,46 @@ public final class AudioWaveForm {
|
|||
codec.release();
|
||||
extractor.release();
|
||||
|
||||
float[] floats = new float[AudioWaveForm.BARS];
|
||||
float max = 0;
|
||||
for (int i = 0; i < AudioWaveForm.BARS; i++) {
|
||||
float[] floats = new float[BAR_COUNT];
|
||||
byte[] bytes = new byte[BAR_COUNT];
|
||||
float max = 0;
|
||||
|
||||
for (int i = 0; i < BAR_COUNT; i++) {
|
||||
if (waveSamples[i] == 0) continue;
|
||||
|
||||
floats[i] = wave[i] / (float) waveSamples[i];
|
||||
if (floats[i] > max) {
|
||||
max = floats[i];
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < AudioWaveForm.BARS; i++) {
|
||||
floats[i] /= max;
|
||||
|
||||
for (int i = 0; i < BAR_COUNT; i++) {
|
||||
float normalized = floats[i] / max;
|
||||
bytes[i] = (byte) (255 * normalized);
|
||||
}
|
||||
return new AudioFileInfo(totalDurationUs, floats);
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull String requireAudio(@NonNull String mime) {
|
||||
if (!mime.startsWith("audio/")) {
|
||||
throw new AssertionError();
|
||||
return new AudioFileInfo(totalDurationUs, bytes);
|
||||
}
|
||||
|
||||
return mime;
|
||||
}
|
||||
|
||||
public static class AudioFileInfo {
|
||||
private final long durationUs;
|
||||
private final byte[] waveFormBytes;
|
||||
private final float[] waveForm;
|
||||
|
||||
private AudioFileInfo(long durationUs, float[] waveForm) {
|
||||
this.durationUs = durationUs;
|
||||
this.waveForm = waveForm;
|
||||
private static @NonNull AudioFileInfo fromDatabaseProtobuf(@NonNull AudioWaveFormData audioWaveForm) {
|
||||
return new AudioFileInfo(audioWaveForm.getDurationUs(), audioWaveForm.getWaveForm().toByteArray());
|
||||
}
|
||||
|
||||
private AudioFileInfo(long durationUs, byte[] waveFormBytes) {
|
||||
this.durationUs = durationUs;
|
||||
this.waveFormBytes = waveFormBytes;
|
||||
this.waveForm = new float[waveFormBytes.length];
|
||||
|
||||
for (int i = 0; i < waveFormBytes.length; i++) {
|
||||
int unsigned = waveFormBytes[i] & 0xff;
|
||||
this.waveForm[i] = unsigned / 255f;
|
||||
}
|
||||
}
|
||||
|
||||
public long getDuration(@NonNull TimeUnit timeUnit) {
|
||||
|
@ -228,5 +293,12 @@ public final class AudioWaveForm {
|
|||
public float[] getWaveForm() {
|
||||
return waveForm;
|
||||
}
|
||||
|
||||
private @NonNull AudioWaveFormData toDatabaseProtobuf() {
|
||||
return AudioWaveFormData.newBuilder()
|
||||
.setDurationUs(durationUs)
|
||||
.setWaveForm(ByteString.copyFrom(waveFormBytes))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import android.graphics.Rect;
|
|||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.SeekBar;
|
||||
|
@ -38,7 +37,7 @@ import org.thoughtcrime.securesms.mms.AudioSlide;
|
|||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class AudioView extends FrameLayout implements AudioSlidePlayer.Listener {
|
||||
|
@ -49,7 +48,6 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||
private static final int REVERSE = -1;
|
||||
|
||||
@NonNull private final AnimatingToggle controlToggle;
|
||||
@NonNull private final ViewGroup container;
|
||||
@NonNull private final View progressAndPlay;
|
||||
@NonNull private final LottieAnimationView playPauseButton;
|
||||
@NonNull private final ImageView downloadButton;
|
||||
|
@ -58,7 +56,6 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||
private final boolean smallView;
|
||||
private final boolean autoRewind;
|
||||
|
||||
@Nullable private final TextView timestamp;
|
||||
@Nullable private final TextView duration;
|
||||
|
||||
@ColorInt private final int waveFormPlayedBarsColor;
|
||||
|
@ -69,6 +66,7 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||
private int backwardsCounter;
|
||||
private int lottieDirection;
|
||||
private boolean isPlaying;
|
||||
private long durationMillis;
|
||||
|
||||
public AudioView(Context context) {
|
||||
this(context, null);
|
||||
|
@ -89,14 +87,12 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||
|
||||
inflate(context, smallView ? R.layout.audio_view_small : R.layout.audio_view, this);
|
||||
|
||||
this.container = findViewById(R.id.audio_widget_container);
|
||||
this.controlToggle = findViewById(R.id.control_toggle);
|
||||
this.playPauseButton = findViewById(R.id.play);
|
||||
this.progressAndPlay = findViewById(R.id.progress_and_play);
|
||||
this.downloadButton = findViewById(R.id.download);
|
||||
this.circleProgress = findViewById(R.id.circle_progress);
|
||||
this.seekBar = findViewById(R.id.seek);
|
||||
this.timestamp = findViewById(R.id.timestamp);
|
||||
this.duration = findViewById(R.id.duration);
|
||||
|
||||
lottieDirection = REVERSE;
|
||||
|
@ -108,8 +104,6 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||
|
||||
this.waveFormPlayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformPlayedBarsColor, Color.WHITE);
|
||||
this.waveFormUnplayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformUnplayedBarsColor, Color.WHITE);
|
||||
|
||||
container.setBackgroundColor(typedArray.getColor(R.styleable.AudioView_widgetBackground, Color.TRANSPARENT));
|
||||
} finally {
|
||||
if (typedArray != null) {
|
||||
typedArray.recycle();
|
||||
|
@ -132,6 +126,14 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||
public void setAudio(final @NonNull AudioSlide audio,
|
||||
final boolean showControls)
|
||||
{
|
||||
if (seekBar instanceof WaveFormSeekBarView) {
|
||||
if (audioSlidePlayer != null && !Objects.equals(audioSlidePlayer.getAudioSlide().getUri(), audio.getUri())) {
|
||||
WaveFormSeekBarView waveFormView = (WaveFormSeekBarView) seekBar;
|
||||
waveFormView.setWaveMode(false);
|
||||
seekBar.setProgress(0);
|
||||
durationMillis = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (showControls && audio.isPendingDownload()) {
|
||||
controlToggle.displayQuick(downloadButton);
|
||||
|
@ -157,14 +159,14 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||
WaveFormSeekBarView waveFormView = (WaveFormSeekBarView) seekBar;
|
||||
waveFormView.setColors(waveFormPlayedBarsColor, waveFormUnplayedBarsColor);
|
||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
||||
new AudioWaveForm(getContext(), audio).generateWaveForm(
|
||||
new AudioWaveForm(getContext(), audio).getWaveForm(
|
||||
data -> {
|
||||
waveFormView.setWaveData(data.getWaveForm());
|
||||
if (duration != null) {
|
||||
long durationSecs = data.getDuration(TimeUnit.SECONDS);
|
||||
duration.setText(getContext().getResources().getString(R.string.AudioView_duration, durationSecs / 60, durationSecs % 60));
|
||||
durationMillis = data.getDuration(TimeUnit.MILLISECONDS);
|
||||
updateProgress(0, 0);
|
||||
duration.setVisibility(VISIBLE);
|
||||
}
|
||||
waveFormView.setWaveData(data.getWaveForm());
|
||||
},
|
||||
e -> waveFormView.setWaveMode(false));
|
||||
} else {
|
||||
|
@ -243,10 +245,9 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||
}
|
||||
|
||||
private void updateProgress(float progress, long millis) {
|
||||
if (timestamp != null) {
|
||||
timestamp.setText(String.format(Locale.getDefault(), "%02d:%02d",
|
||||
TimeUnit.MILLISECONDS.toMinutes(millis),
|
||||
TimeUnit.MILLISECONDS.toSeconds(millis)));
|
||||
if (duration != null && durationMillis > 0) {
|
||||
long remainingSecs = TimeUnit.MILLISECONDS.toSeconds(durationMillis - millis);
|
||||
duration.setText(getResources().getString(R.string.AudioView_duration, remainingSecs / 60, remainingSecs % 60));
|
||||
}
|
||||
|
||||
if (smallView) {
|
||||
|
@ -262,9 +263,6 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||
this.downloadButton.setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
|
||||
this.circleProgress.setBarColor(foregroundTint);
|
||||
|
||||
if (this.timestamp != null) {
|
||||
this.timestamp.setTextColor(foregroundTint);
|
||||
}
|
||||
if (this.duration != null) {
|
||||
this.duration.setTextColor(foregroundTint);
|
||||
}
|
||||
|
@ -372,7 +370,12 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||
private boolean wasPlaying;
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (fromUser && durationMillis > 0) {
|
||||
float progressFloat = progress / (float) seekBar.getMax();
|
||||
updateProgress(progressFloat, (long) (durationMillis * progressFloat));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onStartTrackingTouch(SeekBar seekBar) {
|
||||
|
|
|
@ -88,9 +88,8 @@ public final class WaveFormSeekBarView extends AppCompatSeekBar {
|
|||
if (!Arrays.equals(data, this.data)) {
|
||||
this.data = data;
|
||||
this.dataSetTime = System.currentTimeMillis();
|
||||
setWaveMode(data.length > 0);
|
||||
invalidate();
|
||||
}
|
||||
setWaveMode(data.length > 0);
|
||||
}
|
||||
|
||||
public void setWaveMode(boolean waveMode) {
|
||||
|
@ -101,7 +100,9 @@ public final class WaveFormSeekBarView extends AppCompatSeekBar {
|
|||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
drawWave(canvas);
|
||||
if (waveMode) {
|
||||
drawWave(canvas);
|
||||
}
|
||||
super.onDraw(canvas);
|
||||
}
|
||||
|
||||
|
|
|
@ -643,7 +643,7 @@ public class Contact implements Parcelable {
|
|||
|
||||
private static Attachment attachmentFromUri(@Nullable Uri uri) {
|
||||
if (uri == null) return null;
|
||||
return new UriAttachment(uri, MediaUtil.IMAGE_JPEG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, false, false, null, null, null, null);
|
||||
return new UriAttachment(uri, MediaUtil.IMAGE_JPEG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, false, false, null, null, null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -31,6 +31,7 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
|
@ -44,12 +45,14 @@ import org.json.JSONException;
|
|||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.audio.AudioHash;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
||||
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
|
||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
|
||||
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.MediaStream;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
|
@ -120,7 +123,7 @@ public class AttachmentDatabase extends Database {
|
|||
static final String HEIGHT = "height";
|
||||
static final String CAPTION = "caption";
|
||||
private static final String DATA_HASH = "data_hash";
|
||||
static final String BLUR_HASH = "blur_hash";
|
||||
static final String VISUAL_HASH = "blur_hash";
|
||||
static final String TRANSFORM_PROPERTIES = "transform_properties";
|
||||
static final String DISPLAY_ORDER = "display_order";
|
||||
static final String UPLOAD_TIMESTAMP = "upload_timestamp";
|
||||
|
@ -145,7 +148,7 @@ public class AttachmentDatabase extends Database {
|
|||
THUMBNAIL_ASPECT_RATIO, UNIQUE_ID, DIGEST,
|
||||
FAST_PREFLIGHT_ID, VOICE_NOTE, QUOTE, DATA_RANDOM,
|
||||
THUMBNAIL_RANDOM, WIDTH, HEIGHT, CAPTION, STICKER_PACK_ID,
|
||||
STICKER_PACK_KEY, STICKER_ID, DATA_HASH, BLUR_HASH,
|
||||
STICKER_PACK_KEY, STICKER_ID, DATA_HASH, VISUAL_HASH,
|
||||
TRANSFORM_PROPERTIES, TRANSFER_FILE, DISPLAY_ORDER,
|
||||
UPLOAD_TIMESTAMP };
|
||||
|
||||
|
@ -182,7 +185,7 @@ public class AttachmentDatabase extends Database {
|
|||
STICKER_PACK_KEY + " DEFAULT NULL, " +
|
||||
STICKER_ID + " INTEGER DEFAULT -1, " +
|
||||
DATA_HASH + " TEXT DEFAULT NULL, " +
|
||||
BLUR_HASH + " TEXT DEFAULT NULL, " +
|
||||
VISUAL_HASH + " TEXT DEFAULT NULL, " +
|
||||
TRANSFORM_PROPERTIES + " TEXT DEFAULT NULL, " +
|
||||
TRANSFER_FILE + " TEXT DEFAULT NULL, " +
|
||||
DISPLAY_ORDER + " INTEGER DEFAULT 0, " +
|
||||
|
@ -417,7 +420,7 @@ public class AttachmentDatabase extends Database {
|
|||
values.put(WIDTH, 0);
|
||||
values.put(HEIGHT, 0);
|
||||
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE);
|
||||
values.put(BLUR_HASH, (String) null);
|
||||
values.put(VISUAL_HASH, (String) null);
|
||||
values.put(CONTENT_TYPE, MediaUtil.VIEW_ONCE);
|
||||
|
||||
database.update(TABLE_NAME, values, MMS_ID + " = ?", new String[] {mmsId + ""});
|
||||
|
@ -530,8 +533,9 @@ public class AttachmentDatabase extends Database {
|
|||
values.put(DATA_HASH, dataInfo.hash);
|
||||
}
|
||||
|
||||
if (placeholder != null && placeholder.getBlurHash() != null) {
|
||||
values.put(BLUR_HASH, placeholder.getBlurHash().getHash());
|
||||
String visualHashString = getVisualHashStringOrNull(placeholder);
|
||||
if (visualHashString != null) {
|
||||
values.put(VISUAL_HASH, visualHashString);
|
||||
}
|
||||
|
||||
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE);
|
||||
|
@ -555,9 +559,11 @@ public class AttachmentDatabase extends Database {
|
|||
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId, STANDARD_THUMB_TIME));
|
||||
}
|
||||
|
||||
private static @Nullable String getBlurHashStringOrNull(@Nullable BlurHash blurHash) {
|
||||
if (blurHash == null) return null;
|
||||
return blurHash.getHash();
|
||||
private static @Nullable String getVisualHashStringOrNull(@Nullable Attachment attachment) {
|
||||
if (attachment == null) return null;
|
||||
else if (attachment.getBlurHash() != null) return attachment.getBlurHash().getHash();
|
||||
else if (attachment.getAudioHash() != null) return attachment.getAudioHash().getHash();
|
||||
else return null;
|
||||
}
|
||||
|
||||
public void copyAttachmentData(@NonNull AttachmentId sourceId, @NonNull AttachmentId destinationId)
|
||||
|
@ -594,7 +600,7 @@ public class AttachmentDatabase extends Database {
|
|||
contentValues.put(WIDTH, sourceAttachment.getWidth());
|
||||
contentValues.put(HEIGHT, sourceAttachment.getHeight());
|
||||
contentValues.put(CONTENT_TYPE, sourceAttachment.getContentType());
|
||||
contentValues.put(BLUR_HASH, getBlurHashStringOrNull(sourceAttachment.getBlurHash()));
|
||||
contentValues.put(VISUAL_HASH, getVisualHashStringOrNull(sourceAttachment));
|
||||
|
||||
database.update(TABLE_NAME, contentValues, PART_ID_WHERE, destinationId.toStrings());
|
||||
}
|
||||
|
@ -638,7 +644,7 @@ public class AttachmentDatabase extends Database {
|
|||
values.put(NAME, attachment.getRelay());
|
||||
values.put(SIZE, attachment.getSize());
|
||||
values.put(FAST_PREFLIGHT_ID, attachment.getFastPreflightId());
|
||||
values.put(BLUR_HASH, getBlurHashStringOrNull(attachment.getBlurHash()));
|
||||
values.put(VISUAL_HASH, getVisualHashStringOrNull(attachment));
|
||||
values.put(UPLOAD_TIMESTAMP, uploadTimestamp);
|
||||
|
||||
if (dataInfo != null && dataInfo.hash != null) {
|
||||
|
@ -1099,11 +1105,12 @@ public class AttachmentDatabase extends Database {
|
|||
JsonUtils.SaneJSONObject object = new JsonUtils.SaneJSONObject(array.getJSONObject(i));
|
||||
|
||||
if (!object.isNull(ROW_ID)) {
|
||||
String contentType = object.getString(CONTENT_TYPE);
|
||||
result.add(new DatabaseAttachment(new AttachmentId(object.getLong(ROW_ID), object.getLong(UNIQUE_ID)),
|
||||
object.getLong(MMS_ID),
|
||||
!TextUtils.isEmpty(object.getString(DATA)),
|
||||
!TextUtils.isEmpty(object.getString(THUMBNAIL)),
|
||||
object.getString(CONTENT_TYPE),
|
||||
contentType,
|
||||
object.getInt(TRANSFER_STATE),
|
||||
object.getLong(SIZE),
|
||||
object.getString(FILE_NAME),
|
||||
|
@ -1123,7 +1130,8 @@ public class AttachmentDatabase extends Database {
|
|||
object.getString(STICKER_PACK_KEY),
|
||||
object.getInt(STICKER_ID))
|
||||
: null,
|
||||
BlurHash.parseOrNull(object.getString(BLUR_HASH)),
|
||||
MediaUtil.isAudioType(contentType) ? null : BlurHash.parseOrNull(object.getString(VISUAL_HASH)),
|
||||
MediaUtil.isAudioType(contentType) ? AudioHash.parseOrNull(object.getString(VISUAL_HASH)) : null,
|
||||
TransformProperties.parse(object.getString(TRANSFORM_PROPERTIES)),
|
||||
object.getInt(DISPLAY_ORDER),
|
||||
object.getLong(UPLOAD_TIMESTAMP)));
|
||||
|
@ -1132,12 +1140,13 @@ public class AttachmentDatabase extends Database {
|
|||
|
||||
return result;
|
||||
} else {
|
||||
String contentType = cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE));
|
||||
return Collections.singletonList(new DatabaseAttachment(new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(ROW_ID)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)),
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow(DATA)),
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow(THUMBNAIL)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)),
|
||||
contentType,
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME)),
|
||||
|
@ -1157,7 +1166,8 @@ public class AttachmentDatabase extends Database {
|
|||
cursor.getString(cursor.getColumnIndexOrThrow(STICKER_PACK_KEY)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(STICKER_ID)))
|
||||
: null,
|
||||
BlurHash.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(BLUR_HASH))),
|
||||
MediaUtil.isAudioType(contentType) ? null : BlurHash.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(VISUAL_HASH))),
|
||||
MediaUtil.isAudioType(contentType) ? AudioHash.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(VISUAL_HASH))) : null,
|
||||
TransformProperties.parse(cursor.getString(cursor.getColumnIndexOrThrow(TRANSFORM_PROPERTIES))),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(DISPLAY_ORDER)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(UPLOAD_TIMESTAMP))));
|
||||
|
@ -1167,7 +1177,6 @@ public class AttachmentDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private AttachmentId insertAttachment(long mmsId, Attachment attachment, boolean quote)
|
||||
throws MmsException
|
||||
{
|
||||
|
@ -1219,11 +1228,11 @@ public class AttachmentDatabase extends Database {
|
|||
contentValues.put(CAPTION, attachment.getCaption());
|
||||
contentValues.put(UPLOAD_TIMESTAMP, useTemplateUpload ? template.getUploadTimestamp() : attachment.getUploadTimestamp());
|
||||
if (attachment.getTransformProperties().isVideoEdited()) {
|
||||
contentValues.putNull(BLUR_HASH);
|
||||
contentValues.putNull(VISUAL_HASH);
|
||||
contentValues.put(TRANSFORM_PROPERTIES, attachment.getTransformProperties().serialize());
|
||||
thumbnailTimeUs = Math.max(STANDARD_THUMB_TIME, attachment.getTransformProperties().videoTrimStartTimeUs);
|
||||
} else {
|
||||
contentValues.put(BLUR_HASH, getBlurHashStringOrNull(template.getBlurHash()));
|
||||
contentValues.put(VISUAL_HASH, getVisualHashStringOrNull(template));
|
||||
contentValues.put(TRANSFORM_PROPERTIES, template.getTransformProperties().serialize());
|
||||
thumbnailTimeUs = STANDARD_THUMB_TIME;
|
||||
}
|
||||
|
@ -1330,6 +1339,21 @@ public class AttachmentDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public void writeAudioHash(@NonNull AttachmentId attachmentId, @Nullable AudioWaveFormData audioWaveForm) {
|
||||
Log.i(TAG, "updating part audio wave form for #" + attachmentId);
|
||||
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
ContentValues values = new ContentValues(1);
|
||||
|
||||
if (audioWaveForm != null) {
|
||||
values.put(VISUAL_HASH, new AudioHash(audioWaveForm).getHash());
|
||||
} else {
|
||||
values.putNull(VISUAL_HASH);
|
||||
}
|
||||
|
||||
database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
class ThumbnailFetchCallable implements Callable<InputStream> {
|
||||
|
|
|
@ -43,7 +43,7 @@ public class MediaDatabase extends Database {
|
|||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.BLUR_HASH + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VISUAL_HASH + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFORM_PROPERTIES + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CAPTION + ", "
|
||||
|
|
|
@ -218,7 +218,7 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
"'" + AttachmentDatabase.STICKER_PACK_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID+ ", " +
|
||||
"'" + AttachmentDatabase.STICKER_PACK_KEY + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " +
|
||||
"'" + AttachmentDatabase.STICKER_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", " +
|
||||
"'" + AttachmentDatabase.BLUR_HASH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.BLUR_HASH + ", " +
|
||||
"'" + AttachmentDatabase.VISUAL_HASH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VISUAL_HASH + ", " +
|
||||
"'" + AttachmentDatabase.TRANSFORM_PROPERTIES + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFORM_PROPERTIES + ", " +
|
||||
"'" + AttachmentDatabase.DISPLAY_ORDER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", " +
|
||||
"'" + AttachmentDatabase.UPLOAD_TIMESTAMP + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UPLOAD_TIMESTAMP +
|
||||
|
|
|
@ -28,7 +28,6 @@ import net.sqlcipher.database.SQLiteQueryBuilder;
|
|||
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
@ -387,7 +386,7 @@ public class MmsSmsDatabase extends Database {
|
|||
"'" + AttachmentDatabase.STICKER_PACK_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID + ", " +
|
||||
"'" + AttachmentDatabase.STICKER_PACK_KEY + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " +
|
||||
"'" + AttachmentDatabase.STICKER_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", " +
|
||||
"'" + AttachmentDatabase.BLUR_HASH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.BLUR_HASH + ", " +
|
||||
"'" + AttachmentDatabase.VISUAL_HASH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VISUAL_HASH + ", " +
|
||||
"'" + AttachmentDatabase.TRANSFORM_PROPERTIES + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFORM_PROPERTIES + ", " +
|
||||
"'" + AttachmentDatabase.DISPLAY_ORDER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", " +
|
||||
"'" + AttachmentDatabase.UPLOAD_TIMESTAMP + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UPLOAD_TIMESTAMP +
|
||||
|
|
|
@ -138,7 +138,7 @@ final class GroupManagerV1 {
|
|||
|
||||
if (avatar != null) {
|
||||
Uri avatarUri = BlobProvider.getInstance().forData(avatar).createForSingleUseInMemory();
|
||||
avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null, null, null, null);
|
||||
avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null, null, null, null, null);
|
||||
}
|
||||
|
||||
OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, false, null, Collections.emptyList(), Collections.emptyList());
|
||||
|
|
|
@ -229,7 +229,7 @@ public class MmsDownloadJob extends BaseJob {
|
|||
|
||||
attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()),
|
||||
AttachmentDatabase.TRANSFER_PROGRESS_DONE,
|
||||
part.getData().length, name, false, false, null, null, null, null));
|
||||
part.getData().length, name, false, false, null, null, null, null, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1634,6 +1634,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
null,
|
||||
stickerLocator,
|
||||
null,
|
||||
null,
|
||||
null));
|
||||
} else {
|
||||
return Optional.of(PointerAttachment.forPointer(Optional.of(sticker.get().getAttachment()), stickerLocator).get());
|
||||
|
|
|
@ -176,6 +176,7 @@ public class LinkPreviewRepository {
|
|||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null));
|
||||
|
||||
callback.onComplete(thumbnail);
|
||||
|
@ -250,6 +251,7 @@ public class LinkPreviewRepository {
|
|||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null));
|
||||
|
||||
callback.onComplete(Optional.of(new LinkPreview(packUrl, title, thumbnail)));
|
||||
|
|
|
@ -35,11 +35,11 @@ import org.thoughtcrime.securesms.util.ResUtil;
|
|||
public class AudioSlide extends Slide {
|
||||
|
||||
public AudioSlide(Context context, Uri uri, long dataSize, boolean voiceNote) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.AUDIO_UNSPECIFIED, dataSize, 0, 0, false, null, null, null, null, voiceNote, false));
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.AUDIO_UNSPECIFIED, dataSize, 0, 0, false, null, null, null, null, null, voiceNote, false));
|
||||
}
|
||||
|
||||
public AudioSlide(Context context, Uri uri, long dataSize, String contentType, boolean voiceNote) {
|
||||
super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, null, null, null, null));
|
||||
super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, null, null, null, null, null));
|
||||
}
|
||||
|
||||
public AudioSlide(Context context, Attachment attachment) {
|
||||
|
|
|
@ -20,7 +20,7 @@ public class DocumentSlide extends Slide {
|
|||
@NonNull String contentType, long size,
|
||||
@Nullable String fileName)
|
||||
{
|
||||
super(context, constructAttachmentFromUri(context, uri, contentType, size, 0, 0, true, StorageUtil.getCleanFileName(fileName), null, null, null, false, false));
|
||||
super(context, constructAttachmentFromUri(context, uri, contentType, size, 0, 0, true, StorageUtil.getCleanFileName(fileName), null, null, null, null, false, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -20,7 +20,7 @@ public class GifSlide extends ImageSlide {
|
|||
}
|
||||
|
||||
public GifSlide(Context context, Uri uri, long size, int width, int height, @Nullable String caption) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_GIF, size, width, height, true, null, caption, null, null, false, false));
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_GIF, size, width, height, true, null, caption, null, null, null, false, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -42,7 +42,7 @@ public class ImageSlide extends Slide {
|
|||
}
|
||||
|
||||
public ImageSlide(Context context, Uri uri, long size, int width, int height, @Nullable String caption, @Nullable BlurHash blurHash) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_JPEG, size, width, height, true, null, caption, null, blurHash, false, false));
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_JPEG, size, width, height, true, null, caption, null, blurHash, null, false, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -26,6 +26,7 @@ import androidx.annotation.Nullable;
|
|||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment;
|
||||
import org.thoughtcrime.securesms.audio.AudioHash;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
|
@ -155,10 +156,11 @@ public abstract class Slide {
|
|||
@Nullable String caption,
|
||||
@Nullable StickerLocator stickerLocator,
|
||||
@Nullable BlurHash blurHash,
|
||||
@Nullable AudioHash audioHash,
|
||||
boolean voiceNote,
|
||||
boolean quote)
|
||||
{
|
||||
return constructAttachmentFromUri(context, uri, defaultMime, size, width, height, hasThumbnail, fileName, caption, stickerLocator, blurHash, voiceNote, quote, null);
|
||||
return constructAttachmentFromUri(context, uri, defaultMime, size, width, height, hasThumbnail, fileName, caption, stickerLocator, blurHash, audioHash, voiceNote, quote, null);
|
||||
}
|
||||
|
||||
protected static Attachment constructAttachmentFromUri(@NonNull Context context,
|
||||
|
@ -172,6 +174,7 @@ public abstract class Slide {
|
|||
@Nullable String caption,
|
||||
@Nullable StickerLocator stickerLocator,
|
||||
@Nullable BlurHash blurHash,
|
||||
@Nullable AudioHash audioHash,
|
||||
boolean voiceNote,
|
||||
boolean quote,
|
||||
@Nullable AttachmentDatabase.TransformProperties transformProperties)
|
||||
|
@ -192,6 +195,7 @@ public abstract class Slide {
|
|||
caption,
|
||||
stickerLocator,
|
||||
blurHash,
|
||||
audioHash,
|
||||
transformProperties);
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ public class StickerSlide extends Slide {
|
|||
}
|
||||
|
||||
public StickerSlide(Context context, Uri uri, long size, @NonNull StickerLocator stickerLocator) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_WEBP, size, WIDTH, HEIGHT, true, null, null, stickerLocator, null, false, false));
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_WEBP, size, WIDTH, HEIGHT, true, null, null, stickerLocator, null, null, false, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -17,6 +17,6 @@ public class TextSlide extends Slide {
|
|||
}
|
||||
|
||||
public TextSlide(@NonNull Context context, @NonNull Uri uri, @Nullable String filename, long size) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.LONG_TEXT, size, 0, 0, true, filename, null, null, null, false, false));
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.LONG_TEXT, size, 0, 0, true, filename, null, null, null, null, false, false));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ public class VideoSlide extends Slide {
|
|||
}
|
||||
|
||||
public VideoSlide(Context context, Uri uri, long dataSize, @Nullable String caption, @Nullable AttachmentDatabase.TransformProperties transformProperties) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, 0, 0, MediaUtil.hasVideoThumbnail(uri), null, caption, null, null, false, false, transformProperties));
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, 0, 0, MediaUtil.hasVideoThumbnail(uri), null, caption, null, null, null, false, false, transformProperties));
|
||||
}
|
||||
|
||||
public VideoSlide(Context context, Attachment attachment) {
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package org.thoughtcrime.securesms.util.concurrent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* From https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Executor.html
|
||||
*/
|
||||
public final class SerialExecutor implements Executor {
|
||||
private final Queue<Runnable> tasks = new ArrayDeque<>();
|
||||
private final Executor executor;
|
||||
private Runnable active;
|
||||
|
||||
public SerialExecutor(@NonNull Executor executor) {
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
public synchronized void execute(final Runnable r) {
|
||||
tasks.offer(() -> {
|
||||
try {
|
||||
r.run();
|
||||
} finally {
|
||||
scheduleNext();
|
||||
}
|
||||
});
|
||||
if (active == null) {
|
||||
scheduleNext();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void scheduleNext() {
|
||||
if ((active = tasks.poll()) != null) {
|
||||
executor.execute(active);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -41,3 +41,8 @@ message TemporalAuthCredentialResponse {
|
|||
message TemporalAuthCredentialResponses {
|
||||
repeated TemporalAuthCredentialResponse credentialResponse = 1;
|
||||
}
|
||||
|
||||
message AudioWaveFormData {
|
||||
int64 durationUs = 1;
|
||||
bytes waveForm = 2;
|
||||
}
|
||||
|
|
|
@ -4,58 +4,36 @@
|
|||
tools:context="org.thoughtcrime.securesms.components.AudioView">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/audio_widget_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:orientation="horizontal"
|
||||
tools:background="#ff00ff">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:orientation="horizontal">
|
||||
<include layout="@layout/audio_view_circle" />
|
||||
|
||||
<include layout="@layout/audio_view_circle" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.WaveFormSeekBarView
|
||||
android:id="@+id/seek"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="8dp"
|
||||
tools:progress="50" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="?conversation_item_sent_text_secondary_color"
|
||||
android:textSize="@dimen/conversation_item_date_text_size"
|
||||
android:visibility="gone"
|
||||
tools:text="00:30"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
<org.thoughtcrime.securesms.components.WaveFormSeekBarView
|
||||
android:id="@+id/seek"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="10dp"
|
||||
tools:progress="50" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/timestamp"
|
||||
android:id="@+id/duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="76dip"
|
||||
android:autoLink="none"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="?conversation_item_sent_text_secondary_color"
|
||||
android:textSize="@dimen/conversation_item_date_text_size"
|
||||
android:visibility="gone"
|
||||
tools:text="00:15"
|
||||
tools:text="00:30"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
tools:context="org.thoughtcrime.securesms.components.AudioView">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/audio_widget_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
|
|
|
@ -39,7 +39,6 @@
|
|||
android:visibility="gone"
|
||||
android:paddingTop="15dp"
|
||||
android:paddingBottom="15dp"
|
||||
app:widgetBackground="?conversation_item_bubble_background"
|
||||
app:foregroundTintColor="@color/grey_500"
|
||||
app:backgroundTintColor="?conversation_item_bubble_background"/>
|
||||
|
||||
|
|
|
@ -336,7 +336,6 @@
|
|||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="AudioView">
|
||||
<attr name="widgetBackground" format="color"/>
|
||||
<attr name="foregroundTintColor" format="color" />
|
||||
<attr name="backgroundTintColor" format="color" />
|
||||
<attr name="waveformPlayedBarsColor" format="color" />
|
||||
|
|
Loading…
Add table
Reference in a new issue