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;
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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 + ", "
|
||||||
|
|
|
@ -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 +
|
||||||
|
|
|
@ -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 +
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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)));
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
message TemporalAuthCredentialResponses {
|
||||||
repeated TemporalAuthCredentialResponse credentialResponse = 1;
|
repeated TemporalAuthCredentialResponse credentialResponse = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message AudioWaveFormData {
|
||||||
|
int64 durationUs = 1;
|
||||||
|
bytes waveForm = 2;
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
Loading…
Add table
Reference in a new issue