Show remaining time on wave form view and cache wave form in database.

This commit is contained in:
Alan Evans 2020-06-01 18:10:10 -03:00 committed by Greyson Parrelli
parent e01838e996
commit 3fec23fd36
33 changed files with 357 additions and 162 deletions

View file

@ -1,9 +1,11 @@
package org.thoughtcrime.securesms.attachments; package org.thoughtcrime.securesms.attachments;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.audio.AudioHash;
import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties; import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
@ -51,6 +53,9 @@ public abstract class Attachment {
@Nullable @Nullable
private final BlurHash blurHash; private final BlurHash blurHash;
@Nullable
private final AudioHash audioHash;
@NonNull @NonNull
private final TransformProperties transformProperties; private final TransformProperties transformProperties;
@ -58,7 +63,7 @@ public abstract class Attachment {
int cdnNumber, @Nullable String location, @Nullable String key, @Nullable String relay, int cdnNumber, @Nullable String location, @Nullable String key, @Nullable String relay,
@Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote, @Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote,
int width, int height, boolean quote, long uploadTimestamp, @Nullable String caption, 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) @Nullable TransformProperties transformProperties)
{ {
this.contentType = contentType; this.contentType = contentType;
@ -79,6 +84,7 @@ public abstract class Attachment {
this.stickerLocator = stickerLocator; this.stickerLocator = stickerLocator;
this.caption = caption; this.caption = caption;
this.blurHash = blurHash; this.blurHash = blurHash;
this.audioHash = audioHash;
this.transformProperties = transformProperties != null ? transformProperties : TransformProperties.empty(); this.transformProperties = transformProperties != null ? transformProperties : TransformProperties.empty();
} }
@ -172,6 +178,10 @@ public abstract class Attachment {
return blurHash; return blurHash;
} }
public @Nullable AudioHash getAudioHash() {
return audioHash;
}
public @Nullable String getCaption() { public @Nullable String getCaption() {
return caption; return caption;
} }

View file

@ -4,6 +4,7 @@ import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.audio.AudioHash;
import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties; import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
import org.thoughtcrime.securesms.mms.PartAuthority; 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, String fileName, int cdnNumber, String location, String key, String relay,
byte[] digest, String fastPreflightId, boolean voiceNote, byte[] digest, String fastPreflightId, boolean voiceNote,
int width, int height, boolean quote, @Nullable String caption, 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, @Nullable TransformProperties transformProperties, int displayOrder,
long uploadTimestamp) 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.attachmentId = attachmentId;
this.hasData = hasData; this.hasData = hasData;
this.hasThumbnail = hasThumbnail; this.hasThumbnail = hasThumbnail;

View file

@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase;
public class MmsNotificationAttachment extends Attachment { public class MmsNotificationAttachment extends Attachment {
public MmsNotificationAttachment(int status, long size) { 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 @Nullable

View file

@ -4,6 +4,7 @@ import android.net.Uri;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.audio.AudioHash;
import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.stickers.StickerLocator; 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, int width, int height, long uploadTimestamp, @Nullable String caption, @Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash) @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 @Nullable

View file

@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase;
public class TombstoneAttachment extends Attachment { public class TombstoneAttachment extends Attachment {
public TombstoneAttachment(@NonNull String contentType, boolean quote) { 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 @Override

View file

@ -5,6 +5,7 @@ import android.net.Uri;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.audio.AudioHash;
import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties; import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
import org.thoughtcrime.securesms.stickers.StickerLocator; 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, public UriAttachment(@NonNull Uri uri, @NonNull String contentType, int transferState, long size,
@Nullable String fileName, boolean voiceNote, boolean quote, @Nullable String caption, @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, public UriAttachment(@NonNull Uri dataUri, @Nullable Uri thumbnailUri,
@NonNull String contentType, int transferState, long size, int width, int height, @NonNull String contentType, int transferState, long size, int width, int height,
@Nullable String fileName, @Nullable String fastPreflightId, @Nullable String fileName, @Nullable String fastPreflightId,
boolean voiceNote, boolean quote, @Nullable String caption, @Nullable StickerLocator stickerLocator, 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.dataUri = dataUri;
this.thumbnailUri = thumbnailUri; this.thumbnailUri = thumbnailUri;
} }

View file

@ -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;
}
}

View file

@ -14,11 +14,18 @@ import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import androidx.core.util.Consumer; 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.logging.Log;
import org.thoughtcrime.securesms.media.DecryptableUriMediaInput; import org.thoughtcrime.securesms.media.DecryptableUriMediaInput;
import org.thoughtcrime.securesms.media.MediaInput; import org.thoughtcrime.securesms.media.MediaInput;
import org.thoughtcrime.securesms.mms.AudioSlide; import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SerialExecutor;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import java.io.IOException; import java.io.IOException;
@ -32,7 +39,7 @@ public final class AudioWaveForm {
private static final String TAG = Log.tag(AudioWaveForm.class); 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 static final int SAMPLES_PER_BAR = 4;
private final Context context; private final Context context;
@ -43,34 +50,68 @@ public final class AudioWaveForm {
this.slide = slide; this.slide = slide;
} }
private static final LruCache<Uri, AudioFileInfo> WAVE_FORM_CACHE = new LruCache<>(200); private static final LruCache<String, AudioFileInfo> WAVE_FORM_CACHE = new LruCache<>(200);
private static final Executor AUDIO_DECODER_EXECUTOR = SignalExecutors.BOUNDED; private static final Executor AUDIO_DECODER_EXECUTOR = new SerialExecutor(SignalExecutors.BOUNDED);
@AnyThread @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(() -> { 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 { try {
long startTime = System.currentTimeMillis(); DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
Uri uri = slide.getUri(); long startTime = System.currentTimeMillis();
if (uri == null) { AudioFileInfo fileInfo = generateWaveForm(uri);
Util.runOnMain(() -> onFailure.accept(null));
return;
}
AudioFileInfo cached = WAVE_FORM_CACHE.get(uri); Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey));
if (cached != null) {
Util.runOnMain(() -> onSuccess.accept(cached));
return;
}
AudioFileInfo fileInfo = generateWaveForm(uri); DatabaseFactory.getAttachmentDatabase(context).writeAudioHash(dbAttachment.getAttachmentId(), fileInfo.toDatabaseProtobuf());
WAVE_FORM_CACHE.put(uri, fileInfo);
Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms", System.currentTimeMillis() - startTime));
WAVE_FORM_CACHE.put(cacheKey, fileInfo);
Util.runOnMain(() -> onSuccess.accept(fileInfo)); Util.runOnMain(() -> onSuccess.accept(fileInfo));
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "", e); Log.w(TAG, "Failed to create audio wave form for " + cacheKey, e);
onFailure.accept(e); onFailure.accept(e);
} }
}); });
@ -83,17 +124,36 @@ public final class AudioWaveForm {
*/ */
@WorkerThread @WorkerThread
@RequiresApi(api = 23) @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)) { try (MediaInput dataSource = DecryptableUriMediaInput.createForUri(context, uri)) {
long[] wave = new long[BARS]; long[] wave = new long[BAR_COUNT];
int[] waveSamples = new int[BARS]; int[] waveSamples = new int[BAR_COUNT];
int[] inputSamples = new int[BARS * SAMPLES_PER_BAR]; int[] inputSamples = new int[BAR_COUNT * SAMPLES_PER_BAR];
MediaExtractor extractor = dataSource.createExtractor(); MediaExtractor extractor = dataSource.createExtractor();
MediaFormat format = extractor.getTrackFormat(0);
long totalDurationUs = format.getLong(MediaFormat.KEY_DURATION); if (extractor.getTrackCount() == 0) {
String mime = requireAudio(format.getString(MediaFormat.KEY_MIME)); throw new IOException("No audio track");
MediaCodec codec = MediaCodec.createDecoderByType(mime); }
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.configure(format, null, null, 0);
codec.start(); codec.start();
@ -158,29 +218,24 @@ public final class AudioWaveForm {
} }
ByteBuffer buf = codecOutputBuffers[outputBufferIndex]; ByteBuffer buf = codecOutputBuffers[outputBufferIndex];
int barIndex = (int) ((wave.length * info.presentationTimeUs) / totalDurationUs) - 1; int barIndex = (int) ((wave.length * info.presentationTimeUs) / totalDurationUs);
long total = 0; long total = 0;
for (int i = 0; i < info.size; i += 2 * 4) { for (int i = 0; i < info.size; i += 2 * 4) {
short aShort = buf.getShort(i); short aShort = buf.getShort(i);
total += Math.abs(aShort); total += Math.abs(aShort);
} }
if (barIndex > 0) { if (barIndex >= 0 && barIndex < wave.length) {
wave[barIndex] += total; wave[barIndex] += total;
waveSamples[barIndex] += info.size / 2; waveSamples[barIndex] += info.size / 2;
} }
codec.releaseOutputBuffer(outputBufferIndex, false); codec.releaseOutputBuffer(outputBufferIndex, false);
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.d(TAG, "saw output EOS.");
sawOutputEOS = true; sawOutputEOS = true;
} }
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
codecOutputBuffers = codec.getOutputBuffers(); codecOutputBuffers = codec.getOutputBuffers();
Log.d(TAG, "output buffers have changed.");
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat oformat = codec.getOutputFormat(); Log.d(TAG, "output format has changed to " + codec.getOutputFormat());
Log.d(TAG, "output format has changed to " + oformat);
} else {
Log.d(TAG, "dequeueOutputBuffer returned " + outputBufferIndex);
} }
} while (outputBufferIndex >= 0); } while (outputBufferIndex >= 0);
} }
@ -189,36 +244,46 @@ public final class AudioWaveForm {
codec.release(); codec.release();
extractor.release(); extractor.release();
float[] floats = new float[AudioWaveForm.BARS]; float[] floats = new float[BAR_COUNT];
float max = 0; byte[] bytes = new byte[BAR_COUNT];
for (int i = 0; i < AudioWaveForm.BARS; i++) { float max = 0;
for (int i = 0; i < BAR_COUNT; i++) {
if (waveSamples[i] == 0) continue;
floats[i] = wave[i] / (float) waveSamples[i]; floats[i] = wave[i] / (float) waveSamples[i];
if (floats[i] > max) { if (floats[i] > max) {
max = floats[i]; 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) { return new AudioFileInfo(totalDurationUs, bytes);
if (!mime.startsWith("audio/")) {
throw new AssertionError();
} }
return mime;
} }
public static class AudioFileInfo { public static class AudioFileInfo {
private final long durationUs; private final long durationUs;
private final byte[] waveFormBytes;
private final float[] waveForm; private final float[] waveForm;
private AudioFileInfo(long durationUs, float[] waveForm) { private static @NonNull AudioFileInfo fromDatabaseProtobuf(@NonNull AudioWaveFormData audioWaveForm) {
this.durationUs = durationUs; return new AudioFileInfo(audioWaveForm.getDurationUs(), audioWaveForm.getWaveForm().toByteArray());
this.waveForm = waveForm; }
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) { public long getDuration(@NonNull TimeUnit timeUnit) {
@ -228,5 +293,12 @@ public final class AudioWaveForm {
public float[] getWaveForm() { public float[] getWaveForm() {
return waveForm; return waveForm;
} }
private @NonNull AudioWaveFormData toDatabaseProtobuf() {
return AudioWaveFormData.newBuilder()
.setDurationUs(durationUs)
.setWaveForm(ByteString.copyFrom(waveFormBytes))
.build();
}
} }
} }

View file

@ -8,7 +8,6 @@ import android.graphics.Rect;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.SeekBar; import android.widget.SeekBar;
@ -38,7 +37,7 @@ import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.SlideClickListener; import org.thoughtcrime.securesms.mms.SlideClickListener;
import java.io.IOException; import java.io.IOException;
import java.util.Locale; import java.util.Objects;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public final class AudioView extends FrameLayout implements AudioSlidePlayer.Listener { 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; private static final int REVERSE = -1;
@NonNull private final AnimatingToggle controlToggle; @NonNull private final AnimatingToggle controlToggle;
@NonNull private final ViewGroup container;
@NonNull private final View progressAndPlay; @NonNull private final View progressAndPlay;
@NonNull private final LottieAnimationView playPauseButton; @NonNull private final LottieAnimationView playPauseButton;
@NonNull private final ImageView downloadButton; @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 smallView;
private final boolean autoRewind; private final boolean autoRewind;
@Nullable private final TextView timestamp;
@Nullable private final TextView duration; @Nullable private final TextView duration;
@ColorInt private final int waveFormPlayedBarsColor; @ColorInt private final int waveFormPlayedBarsColor;
@ -69,6 +66,7 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
private int backwardsCounter; private int backwardsCounter;
private int lottieDirection; private int lottieDirection;
private boolean isPlaying; private boolean isPlaying;
private long durationMillis;
public AudioView(Context context) { public AudioView(Context context) {
this(context, null); 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); 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.controlToggle = findViewById(R.id.control_toggle);
this.playPauseButton = findViewById(R.id.play); this.playPauseButton = findViewById(R.id.play);
this.progressAndPlay = findViewById(R.id.progress_and_play); this.progressAndPlay = findViewById(R.id.progress_and_play);
this.downloadButton = findViewById(R.id.download); this.downloadButton = findViewById(R.id.download);
this.circleProgress = findViewById(R.id.circle_progress); this.circleProgress = findViewById(R.id.circle_progress);
this.seekBar = findViewById(R.id.seek); this.seekBar = findViewById(R.id.seek);
this.timestamp = findViewById(R.id.timestamp);
this.duration = findViewById(R.id.duration); this.duration = findViewById(R.id.duration);
lottieDirection = REVERSE; 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.waveFormPlayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformPlayedBarsColor, Color.WHITE);
this.waveFormUnplayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformUnplayedBarsColor, Color.WHITE); this.waveFormUnplayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformUnplayedBarsColor, Color.WHITE);
container.setBackgroundColor(typedArray.getColor(R.styleable.AudioView_widgetBackground, Color.TRANSPARENT));
} finally { } finally {
if (typedArray != null) { if (typedArray != null) {
typedArray.recycle(); typedArray.recycle();
@ -132,6 +126,14 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
public void setAudio(final @NonNull AudioSlide audio, public void setAudio(final @NonNull AudioSlide audio,
final boolean showControls) 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()) { if (showControls && audio.isPendingDownload()) {
controlToggle.displayQuick(downloadButton); controlToggle.displayQuick(downloadButton);
@ -157,14 +159,14 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
WaveFormSeekBarView waveFormView = (WaveFormSeekBarView) seekBar; WaveFormSeekBarView waveFormView = (WaveFormSeekBarView) seekBar;
waveFormView.setColors(waveFormPlayedBarsColor, waveFormUnplayedBarsColor); waveFormView.setColors(waveFormPlayedBarsColor, waveFormUnplayedBarsColor);
if (android.os.Build.VERSION.SDK_INT >= 23) { if (android.os.Build.VERSION.SDK_INT >= 23) {
new AudioWaveForm(getContext(), audio).generateWaveForm( new AudioWaveForm(getContext(), audio).getWaveForm(
data -> { data -> {
waveFormView.setWaveData(data.getWaveForm());
if (duration != null) { if (duration != null) {
long durationSecs = data.getDuration(TimeUnit.SECONDS); durationMillis = data.getDuration(TimeUnit.MILLISECONDS);
duration.setText(getContext().getResources().getString(R.string.AudioView_duration, durationSecs / 60, durationSecs % 60)); updateProgress(0, 0);
duration.setVisibility(VISIBLE); duration.setVisibility(VISIBLE);
} }
waveFormView.setWaveData(data.getWaveForm());
}, },
e -> waveFormView.setWaveMode(false)); e -> waveFormView.setWaveMode(false));
} else { } else {
@ -243,10 +245,9 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
} }
private void updateProgress(float progress, long millis) { private void updateProgress(float progress, long millis) {
if (timestamp != null) { if (duration != null && durationMillis > 0) {
timestamp.setText(String.format(Locale.getDefault(), "%02d:%02d", long remainingSecs = TimeUnit.MILLISECONDS.toSeconds(durationMillis - millis);
TimeUnit.MILLISECONDS.toMinutes(millis), duration.setText(getResources().getString(R.string.AudioView_duration, remainingSecs / 60, remainingSecs % 60));
TimeUnit.MILLISECONDS.toSeconds(millis)));
} }
if (smallView) { if (smallView) {
@ -262,9 +263,6 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
this.downloadButton.setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN); this.downloadButton.setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
this.circleProgress.setBarColor(foregroundTint); this.circleProgress.setBarColor(foregroundTint);
if (this.timestamp != null) {
this.timestamp.setTextColor(foregroundTint);
}
if (this.duration != null) { if (this.duration != null) {
this.duration.setTextColor(foregroundTint); this.duration.setTextColor(foregroundTint);
} }
@ -372,7 +370,12 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
private boolean wasPlaying; private boolean wasPlaying;
@Override @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 @Override
public synchronized void onStartTrackingTouch(SeekBar seekBar) { public synchronized void onStartTrackingTouch(SeekBar seekBar) {

View file

@ -88,9 +88,8 @@ public final class WaveFormSeekBarView extends AppCompatSeekBar {
if (!Arrays.equals(data, this.data)) { if (!Arrays.equals(data, this.data)) {
this.data = data; this.data = data;
this.dataSetTime = System.currentTimeMillis(); this.dataSetTime = System.currentTimeMillis();
setWaveMode(data.length > 0);
invalidate();
} }
setWaveMode(data.length > 0);
} }
public void setWaveMode(boolean waveMode) { public void setWaveMode(boolean waveMode) {
@ -101,7 +100,9 @@ public final class WaveFormSeekBarView extends AppCompatSeekBar {
@Override @Override
protected void onDraw(Canvas canvas) { protected void onDraw(Canvas canvas) {
drawWave(canvas); if (waveMode) {
drawWave(canvas);
}
super.onDraw(canvas); super.onDraw(canvas);
} }

View file

@ -643,7 +643,7 @@ public class Contact implements Parcelable {
private static Attachment attachmentFromUri(@Nullable Uri uri) { private static Attachment attachmentFromUri(@Nullable Uri uri) {
if (uri == null) return null; 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 @Override

View file

@ -31,6 +31,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.fasterxml.jackson.annotation.JsonCreator; 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.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.audio.AudioHash;
import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.crypto.AttachmentSecret; import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream; import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; 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.logging.Log;
import org.thoughtcrime.securesms.mms.MediaStream; import org.thoughtcrime.securesms.mms.MediaStream;
import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.MmsException;
@ -120,7 +123,7 @@ public class AttachmentDatabase extends Database {
static final String HEIGHT = "height"; static final String HEIGHT = "height";
static final String CAPTION = "caption"; static final String CAPTION = "caption";
private static final String DATA_HASH = "data_hash"; 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 TRANSFORM_PROPERTIES = "transform_properties";
static final String DISPLAY_ORDER = "display_order"; static final String DISPLAY_ORDER = "display_order";
static final String UPLOAD_TIMESTAMP = "upload_timestamp"; static final String UPLOAD_TIMESTAMP = "upload_timestamp";
@ -145,7 +148,7 @@ public class AttachmentDatabase extends Database {
THUMBNAIL_ASPECT_RATIO, UNIQUE_ID, DIGEST, THUMBNAIL_ASPECT_RATIO, UNIQUE_ID, DIGEST,
FAST_PREFLIGHT_ID, VOICE_NOTE, QUOTE, DATA_RANDOM, FAST_PREFLIGHT_ID, VOICE_NOTE, QUOTE, DATA_RANDOM,
THUMBNAIL_RANDOM, WIDTH, HEIGHT, CAPTION, STICKER_PACK_ID, 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, TRANSFORM_PROPERTIES, TRANSFER_FILE, DISPLAY_ORDER,
UPLOAD_TIMESTAMP }; UPLOAD_TIMESTAMP };
@ -182,7 +185,7 @@ public class AttachmentDatabase extends Database {
STICKER_PACK_KEY + " DEFAULT NULL, " + STICKER_PACK_KEY + " DEFAULT NULL, " +
STICKER_ID + " INTEGER DEFAULT -1, " + STICKER_ID + " INTEGER DEFAULT -1, " +
DATA_HASH + " TEXT DEFAULT NULL, " + DATA_HASH + " TEXT DEFAULT NULL, " +
BLUR_HASH + " TEXT DEFAULT NULL, " + VISUAL_HASH + " TEXT DEFAULT NULL, " +
TRANSFORM_PROPERTIES + " TEXT DEFAULT NULL, " + TRANSFORM_PROPERTIES + " TEXT DEFAULT NULL, " +
TRANSFER_FILE + " TEXT DEFAULT NULL, " + TRANSFER_FILE + " TEXT DEFAULT NULL, " +
DISPLAY_ORDER + " INTEGER DEFAULT 0, " + DISPLAY_ORDER + " INTEGER DEFAULT 0, " +
@ -417,7 +420,7 @@ public class AttachmentDatabase extends Database {
values.put(WIDTH, 0); values.put(WIDTH, 0);
values.put(HEIGHT, 0); values.put(HEIGHT, 0);
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE); 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); values.put(CONTENT_TYPE, MediaUtil.VIEW_ONCE);
database.update(TABLE_NAME, values, MMS_ID + " = ?", new String[] {mmsId + ""}); 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); values.put(DATA_HASH, dataInfo.hash);
} }
if (placeholder != null && placeholder.getBlurHash() != null) { String visualHashString = getVisualHashStringOrNull(placeholder);
values.put(BLUR_HASH, placeholder.getBlurHash().getHash()); if (visualHashString != null) {
values.put(VISUAL_HASH, visualHashString);
} }
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE); values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE);
@ -555,9 +559,11 @@ public class AttachmentDatabase extends Database {
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId, STANDARD_THUMB_TIME)); thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId, STANDARD_THUMB_TIME));
} }
private static @Nullable String getBlurHashStringOrNull(@Nullable BlurHash blurHash) { private static @Nullable String getVisualHashStringOrNull(@Nullable Attachment attachment) {
if (blurHash == null) return null; if (attachment == null) return null;
return blurHash.getHash(); 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) 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(WIDTH, sourceAttachment.getWidth());
contentValues.put(HEIGHT, sourceAttachment.getHeight()); contentValues.put(HEIGHT, sourceAttachment.getHeight());
contentValues.put(CONTENT_TYPE, sourceAttachment.getContentType()); 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()); 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(NAME, attachment.getRelay());
values.put(SIZE, attachment.getSize()); values.put(SIZE, attachment.getSize());
values.put(FAST_PREFLIGHT_ID, attachment.getFastPreflightId()); 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); values.put(UPLOAD_TIMESTAMP, uploadTimestamp);
if (dataInfo != null && dataInfo.hash != null) { if (dataInfo != null && dataInfo.hash != null) {
@ -1099,11 +1105,12 @@ public class AttachmentDatabase extends Database {
JsonUtils.SaneJSONObject object = new JsonUtils.SaneJSONObject(array.getJSONObject(i)); JsonUtils.SaneJSONObject object = new JsonUtils.SaneJSONObject(array.getJSONObject(i));
if (!object.isNull(ROW_ID)) { 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)), result.add(new DatabaseAttachment(new AttachmentId(object.getLong(ROW_ID), object.getLong(UNIQUE_ID)),
object.getLong(MMS_ID), object.getLong(MMS_ID),
!TextUtils.isEmpty(object.getString(DATA)), !TextUtils.isEmpty(object.getString(DATA)),
!TextUtils.isEmpty(object.getString(THUMBNAIL)), !TextUtils.isEmpty(object.getString(THUMBNAIL)),
object.getString(CONTENT_TYPE), contentType,
object.getInt(TRANSFER_STATE), object.getInt(TRANSFER_STATE),
object.getLong(SIZE), object.getLong(SIZE),
object.getString(FILE_NAME), object.getString(FILE_NAME),
@ -1123,7 +1130,8 @@ public class AttachmentDatabase extends Database {
object.getString(STICKER_PACK_KEY), object.getString(STICKER_PACK_KEY),
object.getInt(STICKER_ID)) object.getInt(STICKER_ID))
: null, : 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)), TransformProperties.parse(object.getString(TRANSFORM_PROPERTIES)),
object.getInt(DISPLAY_ORDER), object.getInt(DISPLAY_ORDER),
object.getLong(UPLOAD_TIMESTAMP))); object.getLong(UPLOAD_TIMESTAMP)));
@ -1132,12 +1140,13 @@ public class AttachmentDatabase extends Database {
return result; return result;
} else { } else {
String contentType = cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE));
return Collections.singletonList(new DatabaseAttachment(new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(ROW_ID)), return Collections.singletonList(new DatabaseAttachment(new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(ROW_ID)),
cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))), cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))),
cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)), cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)),
!cursor.isNull(cursor.getColumnIndexOrThrow(DATA)), !cursor.isNull(cursor.getColumnIndexOrThrow(DATA)),
!cursor.isNull(cursor.getColumnIndexOrThrow(THUMBNAIL)), !cursor.isNull(cursor.getColumnIndexOrThrow(THUMBNAIL)),
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)), contentType,
cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)), cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)),
cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)), cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)),
cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME)), cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME)),
@ -1157,7 +1166,8 @@ public class AttachmentDatabase extends Database {
cursor.getString(cursor.getColumnIndexOrThrow(STICKER_PACK_KEY)), cursor.getString(cursor.getColumnIndexOrThrow(STICKER_PACK_KEY)),
cursor.getInt(cursor.getColumnIndexOrThrow(STICKER_ID))) cursor.getInt(cursor.getColumnIndexOrThrow(STICKER_ID)))
: null, : 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))), TransformProperties.parse(cursor.getString(cursor.getColumnIndexOrThrow(TRANSFORM_PROPERTIES))),
cursor.getInt(cursor.getColumnIndexOrThrow(DISPLAY_ORDER)), cursor.getInt(cursor.getColumnIndexOrThrow(DISPLAY_ORDER)),
cursor.getLong(cursor.getColumnIndexOrThrow(UPLOAD_TIMESTAMP)))); cursor.getLong(cursor.getColumnIndexOrThrow(UPLOAD_TIMESTAMP))));
@ -1167,7 +1177,6 @@ public class AttachmentDatabase extends Database {
} }
} }
private AttachmentId insertAttachment(long mmsId, Attachment attachment, boolean quote) private AttachmentId insertAttachment(long mmsId, Attachment attachment, boolean quote)
throws MmsException throws MmsException
{ {
@ -1219,11 +1228,11 @@ public class AttachmentDatabase extends Database {
contentValues.put(CAPTION, attachment.getCaption()); contentValues.put(CAPTION, attachment.getCaption());
contentValues.put(UPLOAD_TIMESTAMP, useTemplateUpload ? template.getUploadTimestamp() : attachment.getUploadTimestamp()); contentValues.put(UPLOAD_TIMESTAMP, useTemplateUpload ? template.getUploadTimestamp() : attachment.getUploadTimestamp());
if (attachment.getTransformProperties().isVideoEdited()) { if (attachment.getTransformProperties().isVideoEdited()) {
contentValues.putNull(BLUR_HASH); contentValues.putNull(VISUAL_HASH);
contentValues.put(TRANSFORM_PROPERTIES, attachment.getTransformProperties().serialize()); contentValues.put(TRANSFORM_PROPERTIES, attachment.getTransformProperties().serialize());
thumbnailTimeUs = Math.max(STANDARD_THUMB_TIME, attachment.getTransformProperties().videoTrimStartTimeUs); thumbnailTimeUs = Math.max(STANDARD_THUMB_TIME, attachment.getTransformProperties().videoTrimStartTimeUs);
} else { } else {
contentValues.put(BLUR_HASH, getBlurHashStringOrNull(template.getBlurHash())); contentValues.put(VISUAL_HASH, getVisualHashStringOrNull(template));
contentValues.put(TRANSFORM_PROPERTIES, template.getTransformProperties().serialize()); contentValues.put(TRANSFORM_PROPERTIES, template.getTransformProperties().serialize());
thumbnailTimeUs = STANDARD_THUMB_TIME; 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 @VisibleForTesting
class ThumbnailFetchCallable implements Callable<InputStream> { class ThumbnailFetchCallable implements Callable<InputStream> {

View file

@ -43,7 +43,7 @@ public class MediaDatabase extends Database {
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", " + 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.TRANSFORM_PROPERTIES + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CAPTION + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CAPTION + ", "

View file

@ -218,7 +218,7 @@ public class MmsDatabase extends MessagingDatabase {
"'" + AttachmentDatabase.STICKER_PACK_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID+ ", " + "'" + AttachmentDatabase.STICKER_PACK_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID+ ", " +
"'" + AttachmentDatabase.STICKER_PACK_KEY + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " + "'" + AttachmentDatabase.STICKER_PACK_KEY + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " +
"'" + AttachmentDatabase.STICKER_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", " + "'" + 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.TRANSFORM_PROPERTIES + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFORM_PROPERTIES + ", " +
"'" + AttachmentDatabase.DISPLAY_ORDER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", " + "'" + AttachmentDatabase.DISPLAY_ORDER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", " +
"'" + AttachmentDatabase.UPLOAD_TIMESTAMP + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UPLOAD_TIMESTAMP + "'" + AttachmentDatabase.UPLOAD_TIMESTAMP + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UPLOAD_TIMESTAMP +

View file

@ -28,7 +28,6 @@ import net.sqlcipher.database.SQLiteQueryBuilder;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.libsignal.util.Pair; 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_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID + ", " +
"'" + AttachmentDatabase.STICKER_PACK_KEY + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " + "'" + AttachmentDatabase.STICKER_PACK_KEY + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " +
"'" + AttachmentDatabase.STICKER_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", " + "'" + 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.TRANSFORM_PROPERTIES + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFORM_PROPERTIES + ", " +
"'" + AttachmentDatabase.DISPLAY_ORDER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", " + "'" + AttachmentDatabase.DISPLAY_ORDER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", " +
"'" + AttachmentDatabase.UPLOAD_TIMESTAMP + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UPLOAD_TIMESTAMP + "'" + AttachmentDatabase.UPLOAD_TIMESTAMP + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UPLOAD_TIMESTAMP +

View file

@ -138,7 +138,7 @@ final class GroupManagerV1 {
if (avatar != null) { if (avatar != null) {
Uri avatarUri = BlobProvider.getInstance().forData(avatar).createForSingleUseInMemory(); 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()); OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, false, null, Collections.emptyList(), Collections.emptyList());

View file

@ -229,7 +229,7 @@ public class MmsDownloadJob extends BaseJob {
attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()), attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()),
AttachmentDatabase.TRANSFER_PROGRESS_DONE, 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));
} }
} }
} }

View file

@ -1634,6 +1634,7 @@ public final class PushProcessMessageJob extends BaseJob {
null, null,
stickerLocator, stickerLocator,
null, null,
null,
null)); null));
} else { } else {
return Optional.of(PointerAttachment.forPointer(Optional.of(sticker.get().getAttachment()), stickerLocator).get()); return Optional.of(PointerAttachment.forPointer(Optional.of(sticker.get().getAttachment()), stickerLocator).get());

View file

@ -176,6 +176,7 @@ public class LinkPreviewRepository {
null, null,
null, null,
null, null,
null,
null)); null));
callback.onComplete(thumbnail); callback.onComplete(thumbnail);
@ -250,6 +251,7 @@ public class LinkPreviewRepository {
null, null,
null, null,
null, null,
null,
null)); null));
callback.onComplete(Optional.of(new LinkPreview(packUrl, title, thumbnail))); callback.onComplete(Optional.of(new LinkPreview(packUrl, title, thumbnail)));

View file

@ -35,11 +35,11 @@ import org.thoughtcrime.securesms.util.ResUtil;
public class AudioSlide extends Slide { public class AudioSlide extends Slide {
public AudioSlide(Context context, Uri uri, long dataSize, boolean voiceNote) { 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) { 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) { public AudioSlide(Context context, Attachment attachment) {

View file

@ -20,7 +20,7 @@ public class DocumentSlide extends Slide {
@NonNull String contentType, long size, @NonNull String contentType, long size,
@Nullable String fileName) @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 @Override

View file

@ -20,7 +20,7 @@ public class GifSlide extends ImageSlide {
} }
public GifSlide(Context context, Uri uri, long size, int width, int height, @Nullable String caption) { 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 @Override

View file

@ -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) { 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 @Override

View file

@ -26,6 +26,7 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.UriAttachment; import org.thoughtcrime.securesms.attachments.UriAttachment;
import org.thoughtcrime.securesms.audio.AudioHash;
import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.stickers.StickerLocator; import org.thoughtcrime.securesms.stickers.StickerLocator;
@ -155,10 +156,11 @@ public abstract class Slide {
@Nullable String caption, @Nullable String caption,
@Nullable StickerLocator stickerLocator, @Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash, @Nullable BlurHash blurHash,
@Nullable AudioHash audioHash,
boolean voiceNote, boolean voiceNote,
boolean quote) 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, protected static Attachment constructAttachmentFromUri(@NonNull Context context,
@ -172,6 +174,7 @@ public abstract class Slide {
@Nullable String caption, @Nullable String caption,
@Nullable StickerLocator stickerLocator, @Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash, @Nullable BlurHash blurHash,
@Nullable AudioHash audioHash,
boolean voiceNote, boolean voiceNote,
boolean quote, boolean quote,
@Nullable AttachmentDatabase.TransformProperties transformProperties) @Nullable AttachmentDatabase.TransformProperties transformProperties)
@ -192,6 +195,7 @@ public abstract class Slide {
caption, caption,
stickerLocator, stickerLocator,
blurHash, blurHash,
audioHash,
transformProperties); transformProperties);
} }

View file

@ -23,7 +23,7 @@ public class StickerSlide extends Slide {
} }
public StickerSlide(Context context, Uri uri, long size, @NonNull StickerLocator stickerLocator) { 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 @Override

View file

@ -17,6 +17,6 @@ public class TextSlide extends Slide {
} }
public TextSlide(@NonNull Context context, @NonNull Uri uri, @Nullable String filename, long size) { 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));
} }
} }

View file

@ -37,7 +37,7 @@ public class VideoSlide extends Slide {
} }
public VideoSlide(Context context, Uri uri, long dataSize, @Nullable String caption, @Nullable AttachmentDatabase.TransformProperties transformProperties) { 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) { public VideoSlide(Context context, Attachment attachment) {

View file

@ -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);
}
}
}

View file

@ -41,3 +41,8 @@ message TemporalAuthCredentialResponse {
message TemporalAuthCredentialResponses { message TemporalAuthCredentialResponses {
repeated TemporalAuthCredentialResponse credentialResponse = 1; repeated TemporalAuthCredentialResponse credentialResponse = 1;
} }
message AudioWaveFormData {
int64 durationUs = 1;
bytes waveForm = 2;
}

View file

@ -4,58 +4,36 @@
tools:context="org.thoughtcrime.securesms.components.AudioView"> tools:context="org.thoughtcrime.securesms.components.AudioView">
<LinearLayout <LinearLayout
android:id="@+id/audio_widget_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="horizontal"
tools:background="#ff00ff"> tools:background="#ff00ff">
<LinearLayout <include layout="@layout/audio_view_circle" />
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal">
<include layout="@layout/audio_view_circle" /> <org.thoughtcrime.securesms.components.WaveFormSeekBarView
android:id="@+id/seek"
<org.thoughtcrime.securesms.components.WaveFormSeekBarView android:layout_width="0dp"
android:id="@+id/seek" android:layout_height="match_parent"
android:layout_width="0dp" android:layout_gravity="center_vertical"
android:layout_weight="1" android:layout_weight="1"
android:layout_height="match_parent" android:paddingStart="12dp"
android:layout_gravity="center_vertical" android:paddingTop="10dp"
android:paddingTop="10dp" android:paddingEnd="8dp"
android:paddingBottom="10dp" android:paddingBottom="10dp"
android:paddingStart="12dp" tools:progress="50" />
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>
<TextView <TextView
android:id="@+id/timestamp" android:id="@+id/duration"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="76dip" android:layout_gravity="center_vertical"
android:autoLink="none"
android:fontFamily="sans-serif-light" android:fontFamily="sans-serif-light"
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?conversation_item_sent_text_secondary_color" android:textColor="?conversation_item_sent_text_secondary_color"
android:textSize="@dimen/conversation_item_date_text_size" android:textSize="@dimen/conversation_item_date_text_size"
android:visibility="gone" android:visibility="gone"
tools:text="00:15" tools:text="00:30"
tools:visibility="visible" /> tools:visibility="visible" />
</LinearLayout> </LinearLayout>

View file

@ -4,7 +4,6 @@
tools:context="org.thoughtcrime.securesms.components.AudioView"> tools:context="org.thoughtcrime.securesms.components.AudioView">
<LinearLayout <LinearLayout
android:id="@+id/audio_widget_container"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"

View file

@ -39,7 +39,6 @@
android:visibility="gone" android:visibility="gone"
android:paddingTop="15dp" android:paddingTop="15dp"
android:paddingBottom="15dp" android:paddingBottom="15dp"
app:widgetBackground="?conversation_item_bubble_background"
app:foregroundTintColor="@color/grey_500" app:foregroundTintColor="@color/grey_500"
app:backgroundTintColor="?conversation_item_bubble_background"/> app:backgroundTintColor="?conversation_item_bubble_background"/>

View file

@ -336,7 +336,6 @@
</declare-styleable> </declare-styleable>
<declare-styleable name="AudioView"> <declare-styleable name="AudioView">
<attr name="widgetBackground" format="color"/>
<attr name="foregroundTintColor" format="color" /> <attr name="foregroundTintColor" format="color" />
<attr name="backgroundTintColor" format="color" /> <attr name="backgroundTintColor" format="color" />
<attr name="waveformPlayedBarsColor" format="color" /> <attr name="waveformPlayedBarsColor" format="color" />