onSuccess, @NonNull Runnable onFailure) {
- Uri uri = slide.getUri();
- Attachment attachment = slide.asAttachment();
-
- if (uri == null) {
- Log.w(TAG, "No uri");
- ThreadUtil.runOnMain(onFailure);
- return;
- }
-
- String cacheKey = uri.toString();
- AudioFileInfo cached = WAVE_FORM_CACHE.get(cacheKey);
- if (cached != null) {
- Log.i(TAG, "Loaded wave form from cache " + cacheKey);
- ThreadUtil.runOnMain(() -> onSuccess.accept(cached));
- return;
- }
-
- AUDIO_DECODER_EXECUTOR.execute(() -> {
- AudioFileInfo cachedInExecutor = WAVE_FORM_CACHE.get(cacheKey);
- if (cachedInExecutor != null) {
- Log.i(TAG, "Loaded wave form from cache inside executor" + cacheKey);
- ThreadUtil.runOnMain(() -> onSuccess.accept(cachedInExecutor));
- return;
- }
-
- AudioHash audioHash = attachment.getAudioHash();
- if (audioHash != null) {
- AudioFileInfo audioFileInfo = AudioFileInfo.fromDatabaseProtobuf(audioHash.getAudioWaveForm());
- if (audioFileInfo.waveForm.length == 0) {
- Log.w(TAG, "Recovering from a wave form generation error " + cacheKey);
- ThreadUtil.runOnMain(onFailure);
- return;
- } else 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);
- ThreadUtil.runOnMain(() -> onSuccess.accept(audioFileInfo));
- return;
- }
- }
-
- if (attachment instanceof DatabaseAttachment) {
- try {
- AttachmentTable attachmentDatabase = SignalDatabase.attachments();
- DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
- long startTime = System.currentTimeMillis();
-
- attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), AudioWaveFormData.getDefaultInstance());
-
- Log.i(TAG, String.format("Starting wave form generation (%s)", cacheKey));
-
- AudioFileInfo fileInfo = generateWaveForm(uri);
-
- Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey));
-
- attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), fileInfo.toDatabaseProtobuf());
-
- WAVE_FORM_CACHE.put(cacheKey, fileInfo);
- ThreadUtil.runOnMain(() -> onSuccess.accept(fileInfo));
- } catch (Throwable e) {
- Log.w(TAG, "Failed to create audio wave form for " + cacheKey, e);
- ThreadUtil.runOnMain(onFailure);
- }
- } else {
- try {
- Log.i(TAG, "Not in database and not cached. Generating wave form on-the-fly.");
-
- long startTime = System.currentTimeMillis();
-
- Log.i(TAG, String.format("Starting wave form generation (%s)", cacheKey));
-
- AudioFileInfo fileInfo = generateWaveForm(uri);
-
- Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey));
-
- WAVE_FORM_CACHE.put(cacheKey, fileInfo);
- ThreadUtil.runOnMain(() -> onSuccess.accept(fileInfo));
- } catch (IOException e) {
- Log.w(TAG, "Failed to create audio wave form for " + cacheKey, e);
- ThreadUtil.runOnMain(onFailure);
- }
- }
- });
- }
-
- /**
- * Based on decode sample from:
- *
- * https://android.googlesource.com/platform/cts/+/jb-mr2-release/tests/tests/media/src/android/media/cts/DecoderTest.java
- */
- @WorkerThread
- @RequiresApi(api = 23)
- private @NonNull AudioFileInfo generateWaveForm(@NonNull Uri uri) throws IOException {
- try (MediaInput dataSource = DecryptableUriMediaInput.createForUri(context, uri)) {
- long[] wave = new long[BAR_COUNT];
- int[] waveSamples = new int[BAR_COUNT];
-
- MediaExtractor extractor = dataSource.createExtractor();
-
- if (extractor.getTrackCount() == 0) {
- throw new IOException("No audio track");
- }
-
- MediaFormat format = extractor.getTrackFormat(0);
-
- if (!format.containsKey(MediaFormat.KEY_DURATION)) {
- throw new IOException("Unknown duration");
- }
-
- long totalDurationUs = format.getLong(MediaFormat.KEY_DURATION);
- String mime = format.getString(MediaFormat.KEY_MIME);
-
- if (!mime.startsWith("audio/")) {
- throw new IOException("Mime not audio");
- }
-
- MediaCodec codec = MediaCodec.createDecoderByType(mime);
-
- if (totalDurationUs == 0) {
- throw new IOException("Zero duration");
- }
-
- codec.configure(format, null, null, 0);
- codec.start();
-
- ByteBuffer[] codecInputBuffers = codec.getInputBuffers();
- ByteBuffer[] codecOutputBuffers = codec.getOutputBuffers();
-
- extractor.selectTrack(0);
-
- long kTimeOutUs = 5000;
- MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
- boolean sawInputEOS = false;
- boolean sawOutputEOS = false;
- int noOutputCounter = 0;
-
- while (!sawOutputEOS && noOutputCounter < 50) {
- noOutputCounter++;
- if (!sawInputEOS) {
- int inputBufIndex = codec.dequeueInputBuffer(kTimeOutUs);
- if (inputBufIndex >= 0) {
- ByteBuffer dstBuf = codecInputBuffers[inputBufIndex];
- int sampleSize = extractor.readSampleData(dstBuf, 0);
- long presentationTimeUs = 0;
-
- if (sampleSize < 0) {
- sawInputEOS = true;
- sampleSize = 0;
- } else {
- presentationTimeUs = extractor.getSampleTime();
- }
-
- codec.queueInputBuffer(
- inputBufIndex,
- 0,
- sampleSize,
- presentationTimeUs,
- sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
-
- if (!sawInputEOS) {
- int barSampleIndex = (int) (SAMPLES_PER_BAR * (wave.length * extractor.getSampleTime()) / totalDurationUs);
- sawInputEOS = !extractor.advance();
- int nextBarSampleIndex = (int) (SAMPLES_PER_BAR * (wave.length * extractor.getSampleTime()) / totalDurationUs);
- while (!sawInputEOS && nextBarSampleIndex == barSampleIndex) {
- sawInputEOS = !extractor.advance();
- if (!sawInputEOS) {
- nextBarSampleIndex = (int) (SAMPLES_PER_BAR * (wave.length * extractor.getSampleTime()) / totalDurationUs);
- }
- }
- }
- }
- }
-
- int outputBufferIndex;
- do {
- outputBufferIndex = codec.dequeueOutputBuffer(info, kTimeOutUs);
- if (outputBufferIndex >= 0) {
- if (info.size > 0) {
- noOutputCounter = 0;
- }
-
- ByteBuffer buf = codecOutputBuffers[outputBufferIndex];
- int barIndex = (int) ((wave.length * info.presentationTimeUs) / totalDurationUs);
- long total = 0;
- for (int i = 0; i < info.size; i += 2 * 4) {
- short aShort = buf.getShort(i);
- total += Math.abs(aShort);
- }
- if (barIndex >= 0 && barIndex < wave.length) {
- wave[barIndex] += total;
- waveSamples[barIndex] += info.size / 2;
- }
- codec.releaseOutputBuffer(outputBufferIndex, false);
- if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
- sawOutputEOS = true;
- }
- } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
- codecOutputBuffers = codec.getOutputBuffers();
- } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
- Log.d(TAG, "output format has changed to " + codec.getOutputFormat());
- }
- } while (outputBufferIndex >= 0);
- }
-
- codec.stop();
- codec.release();
- extractor.release();
-
- float[] floats = new float[BAR_COUNT];
- byte[] bytes = new byte[BAR_COUNT];
- float max = 0;
-
- for (int i = 0; i < BAR_COUNT; i++) {
- if (waveSamples[i] == 0) continue;
-
- floats[i] = wave[i] / (float) waveSamples[i];
- if (floats[i] > max) {
- max = floats[i];
- }
- }
-
- for (int i = 0; i < BAR_COUNT; i++) {
- float normalized = floats[i] / max;
- bytes[i] = (byte) (255 * normalized);
- }
-
- return new AudioFileInfo(totalDurationUs, bytes);
- }
- }
-
- public static class AudioFileInfo {
- private final long durationUs;
- private final byte[] waveFormBytes;
- private final float[] waveForm;
-
- private static @NonNull AudioFileInfo fromDatabaseProtobuf(@NonNull AudioWaveFormData audioWaveForm) {
- return new AudioFileInfo(audioWaveForm.getDurationUs(), audioWaveForm.getWaveForm().toByteArray());
- }
-
- private AudioFileInfo(long durationUs, byte[] waveFormBytes) {
- this.durationUs = durationUs;
- this.waveFormBytes = waveFormBytes;
- this.waveForm = new float[waveFormBytes.length];
-
- for (int i = 0; i < waveFormBytes.length; i++) {
- int unsigned = waveFormBytes[i] & 0xff;
- this.waveForm[i] = unsigned / 255f;
- }
- }
-
- public long getDuration(@NonNull TimeUnit timeUnit) {
- return timeUnit.convert(durationUs, TimeUnit.MICROSECONDS);
- }
-
- public float[] getWaveForm() {
- return waveForm;
- }
-
- private @NonNull AudioWaveFormData toDatabaseProtobuf() {
- return AudioWaveFormData.newBuilder()
- .setDurationUs(durationUs)
- .setWaveForm(ByteString.copyFrom(waveFormBytes))
- .build();
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioWaveFormGenerator.java b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioWaveFormGenerator.java
new file mode 100644
index 0000000000..30e5d0e3d7
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioWaveFormGenerator.java
@@ -0,0 +1,168 @@
+package org.thoughtcrime.securesms.audio;
+
+import android.content.Context;
+import android.media.MediaCodec;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.WorkerThread;
+
+import org.signal.core.util.logging.Log;
+import org.thoughtcrime.securesms.media.DecryptableUriMediaInput;
+import org.thoughtcrime.securesms.media.MediaInput;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+@RequiresApi(api = 23)
+public final class AudioWaveFormGenerator {
+
+ private static final String TAG = Log.tag(AudioWaveFormGenerator.class);
+
+ public static final int BAR_COUNT = 46;
+ private static final int SAMPLES_PER_BAR = 4;
+
+ private AudioWaveFormGenerator() {}
+
+ /**
+ * Based on decode sample from:
+ *
+ * https://android.googlesource.com/platform/cts/+/jb-mr2-release/tests/tests/media/src/android/media/cts/DecoderTest.java
+ */
+ @WorkerThread
+ public static @NonNull AudioFileInfo generateWaveForm(@NonNull Context context, @NonNull Uri uri) throws IOException {
+ try (MediaInput dataSource = DecryptableUriMediaInput.createForUri(context, uri)) {
+ long[] wave = new long[BAR_COUNT];
+ int[] waveSamples = new int[BAR_COUNT];
+
+ MediaExtractor extractor = dataSource.createExtractor();
+
+ if (extractor.getTrackCount() == 0) {
+ throw new IOException("No audio track");
+ }
+
+ MediaFormat format = extractor.getTrackFormat(0);
+
+ if (!format.containsKey(MediaFormat.KEY_DURATION)) {
+ throw new IOException("Unknown duration");
+ }
+
+ long totalDurationUs = format.getLong(MediaFormat.KEY_DURATION);
+ String mime = format.getString(MediaFormat.KEY_MIME);
+
+ if (!mime.startsWith("audio/")) {
+ throw new IOException("Mime not audio");
+ }
+
+ MediaCodec codec = MediaCodec.createDecoderByType(mime);
+
+ if (totalDurationUs == 0) {
+ throw new IOException("Zero duration");
+ }
+
+ codec.configure(format, null, null, 0);
+ codec.start();
+
+ extractor.selectTrack(0);
+
+ long kTimeOutUs = 5000;
+ MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
+ boolean sawInputEOS = false;
+ boolean sawOutputEOS = false;
+ int noOutputCounter = 0;
+
+ while (!sawOutputEOS && noOutputCounter < 50) {
+ noOutputCounter++;
+ if (!sawInputEOS) {
+ int inputBufIndex = codec.dequeueInputBuffer(kTimeOutUs);
+ if (inputBufIndex >= 0) {
+ ByteBuffer dstBuf = codec.getInputBuffer(inputBufIndex);
+ int sampleSize = extractor.readSampleData(dstBuf, 0);
+ long presentationTimeUs = 0;
+
+ if (sampleSize < 0) {
+ sawInputEOS = true;
+ sampleSize = 0;
+ } else {
+ presentationTimeUs = extractor.getSampleTime();
+ }
+
+ codec.queueInputBuffer(
+ inputBufIndex,
+ 0,
+ sampleSize,
+ presentationTimeUs,
+ sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
+
+ if (!sawInputEOS) {
+ int barSampleIndex = (int) (SAMPLES_PER_BAR * (wave.length * extractor.getSampleTime()) / totalDurationUs);
+ sawInputEOS = !extractor.advance();
+ int nextBarSampleIndex = (int) (SAMPLES_PER_BAR * (wave.length * extractor.getSampleTime()) / totalDurationUs);
+ while (!sawInputEOS && nextBarSampleIndex == barSampleIndex) {
+ sawInputEOS = !extractor.advance();
+ if (!sawInputEOS) {
+ nextBarSampleIndex = (int) (SAMPLES_PER_BAR * (wave.length * extractor.getSampleTime()) / totalDurationUs);
+ }
+ }
+ }
+ }
+ }
+
+ int outputBufferIndex;
+ do {
+ outputBufferIndex = codec.dequeueOutputBuffer(info, kTimeOutUs);
+ if (outputBufferIndex >= 0) {
+ if (info.size > 0) {
+ noOutputCounter = 0;
+ }
+
+ ByteBuffer buf = codec.getOutputBuffer(outputBufferIndex);
+ int barIndex = (int) ((wave.length * info.presentationTimeUs) / totalDurationUs);
+ long total = 0;
+ for (int i = 0; i < info.size; i += 2 * 4) {
+ short aShort = buf.getShort(i);
+ total += Math.abs(aShort);
+ }
+ if (barIndex >= 0 && barIndex < wave.length) {
+ wave[barIndex] += total;
+ waveSamples[barIndex] += info.size / 2;
+ }
+ codec.releaseOutputBuffer(outputBufferIndex, false);
+ if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+ sawOutputEOS = true;
+ }
+ } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
+ Log.d(TAG, "output format has changed to " + codec.getOutputFormat());
+ }
+ } while (outputBufferIndex >= 0);
+ }
+
+ codec.stop();
+ codec.release();
+ extractor.release();
+
+ float[] floats = new float[BAR_COUNT];
+ byte[] bytes = new byte[BAR_COUNT];
+ float max = 0;
+
+ for (int i = 0; i < BAR_COUNT; i++) {
+ if (waveSamples[i] == 0) continue;
+
+ floats[i] = wave[i] / (float) waveSamples[i];
+ if (floats[i] > max) {
+ max = floats[i];
+ }
+ }
+
+ for (int i = 0; i < BAR_COUNT; i++) {
+ float normalized = floats[i] / max;
+ bytes[i] = (byte) (255 * normalized);
+ }
+
+ return new AudioFileInfo(totalDurationUs, bytes);
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioWaveForms.kt b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioWaveForms.kt
new file mode 100644
index 0000000000..ff337a824f
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioWaveForms.kt
@@ -0,0 +1,152 @@
+package org.thoughtcrime.securesms.audio
+
+import android.content.Context
+import android.net.Uri
+import android.util.LruCache
+import androidx.annotation.AnyThread
+import androidx.annotation.RequiresApi
+import io.reactivex.rxjava3.core.Single
+import io.reactivex.rxjava3.schedulers.Schedulers
+import org.signal.core.util.logging.Log
+import org.thoughtcrime.securesms.attachments.Attachment
+import org.thoughtcrime.securesms.attachments.AttachmentId
+import org.thoughtcrime.securesms.attachments.DatabaseAttachment
+import org.thoughtcrime.securesms.database.SignalDatabase
+import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData
+import java.io.IOException
+import java.util.concurrent.locks.ReentrantReadWriteLock
+import kotlin.concurrent.read
+import kotlin.concurrent.write
+
+/**
+ * Uses [AudioWaveFormGenerator] to generate audio wave forms.
+ *
+ * Maintains an in-memory cache of recently requested wave forms.
+ */
+@RequiresApi(23)
+object AudioWaveForms {
+
+ private val TAG = Log.tag(AudioWaveForms::class.java)
+
+ private val cache = ThreadSafeLruCache(200)
+
+ @AnyThread
+ @JvmStatic
+ fun getWaveForm(context: Context, attachment: Attachment): Single {
+ val uri = attachment.uri
+ if (uri == null) {
+ Log.i(TAG, "No uri")
+ return Single.error(IllegalArgumentException("No uri from attachment"))
+ }
+
+ val cacheKey = uri.toString()
+ val cachedInfo = cache.get(cacheKey)
+ if (cachedInfo != null) {
+ Log.i(TAG, "Loaded wave form from cache $cacheKey")
+ return Single.just(cachedInfo)
+ }
+
+ val databaseCache = Single.fromCallable {
+ val audioHash = attachment.audioHash
+ return@fromCallable if (audioHash != null) {
+ checkDatabaseCache(cacheKey, audioHash.audioWaveForm)
+ } else {
+ Miss
+ }
+ }.subscribeOn(Schedulers.io())
+
+ val generateWaveForm: Single = if (attachment is DatabaseAttachment) {
+ Single.fromCallable { generateWaveForm(context, uri, cacheKey, attachment.attachmentId) }
+ } else {
+ Single.fromCallable { generateWaveForm(context, uri, cacheKey) }
+ }.subscribeOn(Schedulers.io())
+
+ return databaseCache
+ .flatMap { r ->
+ if (r is Miss) {
+ generateWaveForm
+ } else {
+ Single.just(r)
+ }
+ }
+ .map { r ->
+ if (r is Success) {
+ r.audioFileInfo
+ } else {
+ throw IOException("Unable to generate wave form")
+ }
+ }
+ }
+
+ private fun checkDatabaseCache(cacheKey: String, audioWaveForm: AudioWaveFormData): CacheCheckResult {
+ val audioFileInfo = AudioFileInfo.fromDatabaseProtobuf(audioWaveForm)
+ if (audioFileInfo.waveForm.isEmpty()) {
+ Log.w(TAG, "Recovering from a wave form generation error $cacheKey")
+ return Failure
+ } else if (audioFileInfo.waveForm.size != AudioWaveFormGenerator.BAR_COUNT) {
+ Log.w(TAG, "Wave form from database does not match bar count, regenerating $cacheKey")
+ } else {
+ cache.put(cacheKey, audioFileInfo)
+ Log.i(TAG, "Loaded wave form from DB $cacheKey")
+ return Success(audioFileInfo)
+ }
+
+ return Miss
+ }
+
+ private fun generateWaveForm(context: Context, uri: Uri, cacheKey: String, attachmentId: AttachmentId): CacheCheckResult {
+ try {
+ val startTime = System.currentTimeMillis()
+ SignalDatabase.attachments.writeAudioHash(attachmentId, AudioWaveFormData.getDefaultInstance())
+
+ Log.i(TAG, "Starting wave form generation ($cacheKey)")
+ val fileInfo: AudioFileInfo = AudioWaveFormGenerator.generateWaveForm(context, uri)
+ Log.i(TAG, "Audio wave form generation time ${System.currentTimeMillis() - startTime} ms ($cacheKey)")
+
+ SignalDatabase.attachments.writeAudioHash(attachmentId, fileInfo.toDatabaseProtobuf())
+ cache.put(cacheKey, fileInfo)
+
+ return Success(fileInfo)
+ } catch (e: Throwable) {
+ Log.w(TAG, "Failed to create audio wave form for $cacheKey", e)
+ return Failure
+ }
+ }
+
+ private fun generateWaveForm(context: Context, uri: Uri, cacheKey: String): CacheCheckResult {
+ try {
+ Log.i(TAG, "Not in database and not cached. Generating wave form on-the-fly.")
+
+ val startTime = System.currentTimeMillis()
+
+ Log.i(TAG, "Starting wave form generation ($cacheKey)")
+ val fileInfo: AudioFileInfo = AudioWaveFormGenerator.generateWaveForm(context, uri)
+ Log.i(TAG, "Audio wave form generation time ${System.currentTimeMillis() - startTime} ms ($cacheKey)")
+
+ cache.put(cacheKey, fileInfo)
+
+ return Success(fileInfo)
+ } catch (e: Throwable) {
+ Log.w(TAG, "Failed to create audio wave form for $cacheKey", e)
+ return Failure
+ }
+ }
+
+ private class ThreadSafeLruCache(maxSize: Int) {
+ private val cache = LruCache(maxSize)
+ private val lock = ReentrantReadWriteLock()
+
+ fun put(key: String, info: AudioFileInfo) {
+ lock.write { cache.put(key, info) }
+ }
+
+ fun get(key: String): AudioFileInfo? {
+ return lock.read { cache.get(key) }
+ }
+ }
+
+ private sealed class CacheCheckResult
+ private class Success(val audioFileInfo: AudioFileInfo) : CacheCheckResult()
+ private object Failure : CacheCheckResult()
+ private object Miss : CacheCheckResult()
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/Avatar.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/Avatar.kt
index ae66b114c8..62cf866455 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/avatar/Avatar.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/Avatar.kt
@@ -21,7 +21,7 @@ sealed class Avatar(
data class Text(
val text: String,
val color: Avatars.ColorPair,
- override val databaseId: DatabaseId,
+ override val databaseId: DatabaseId
) : Avatar(databaseId) {
override fun withDatabaseId(databaseId: DatabaseId): Avatar {
return copy(databaseId = databaseId)
@@ -35,7 +35,7 @@ sealed class Avatar(
data class Vector(
val key: String,
val color: Avatars.ColorPair,
- override val databaseId: DatabaseId,
+ override val databaseId: DatabaseId
) : Avatar(databaseId) {
override fun withDatabaseId(databaseId: DatabaseId): Avatar {
return copy(databaseId = databaseId)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarBundler.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarBundler.kt
index 786e2d70b0..32bcdd66d8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarBundler.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarBundler.kt
@@ -1,6 +1,8 @@
package org.thoughtcrime.securesms.avatar
+import android.net.Uri
import android.os.Bundle
+import org.signal.core.util.getParcelableCompat
/**
* Utility class which encapsulates reading and writing Avatar objects to and from Bundles.
@@ -33,7 +35,7 @@ object AvatarBundler {
}
fun extractPhoto(bundle: Bundle): Avatar.Photo = Avatar.Photo(
- uri = requireNotNull(bundle.getParcelable(URI)),
+ uri = requireNotNull(bundle.getParcelableCompat(URI, Uri::class.java)),
size = bundle.getLong(SIZE),
databaseId = bundle.getDatabaseId()
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarRenderer.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarRenderer.kt
index da9cbb4ded..5cf4cbb616 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarRenderer.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarRenderer.kt
@@ -6,6 +6,7 @@ import android.graphics.Canvas
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.net.Uri
+import androidx.annotation.MainThread
import androidx.appcompat.content.res.AppCompatResources
import com.airbnb.lottie.SimpleColorFilter
import org.signal.core.util.concurrent.SignalExecutors
@@ -28,8 +29,13 @@ object AvatarRenderer {
val DIMENSIONS = AvatarHelper.AVATAR_DIMENSIONS
+ private var typeface: Typeface? = null
+
+ @MainThread
fun getTypeface(context: Context): Typeface {
- return Typeface.createFromAsset(context.assets, "fonts/Inter-Medium.otf")
+ val interMedium = typeface ?: Typeface.createFromAsset(context.assets, "fonts/Inter-Medium.otf")
+ typeface = interMedium
+ return interMedium
}
fun renderAvatar(context: Context, avatar: Avatar, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/Avatars.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/Avatars.kt
index 8222e2d474..ec38196e5b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/avatar/Avatars.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/Avatars.kt
@@ -74,7 +74,7 @@ object Avatars {
"avatar_sunset" to DefaultAvatar(R.drawable.ic_avatar_sunset, "A120"),
"avatar_surfboard" to DefaultAvatar(R.drawable.ic_avatar_surfboard, "A110"),
"avatar_soccerball" to DefaultAvatar(R.drawable.ic_avatar_soccerball, "A130"),
- "avatar_football" to DefaultAvatar(R.drawable.ic_avatar_football, "A220"),
+ "avatar_football" to DefaultAvatar(R.drawable.ic_avatar_football, "A220")
)
@DrawableRes
diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerFragment.kt
index 0f8e145a90..1cae56e71c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerFragment.kt
@@ -16,6 +16,7 @@ import androidx.fragment.app.viewModels
import androidx.navigation.Navigation
import androidx.recyclerview.widget.RecyclerView
import org.signal.core.util.ThreadUtil
+import org.signal.core.util.getParcelableExtraCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.avatar.Avatar
import org.thoughtcrime.securesms.avatar.AvatarBundler
@@ -87,8 +88,9 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
val selectedPosition = items.indexOfFirst { it.isSelected }
adapter.submitList(items) {
- if (selectedPosition > -1)
+ if (selectedPosition > -1) {
recycler.smoothScrollToPosition(selectedPosition)
+ }
}
}
@@ -146,10 +148,9 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
ViewUtil.hideKeyboard(requireContext(), requireView())
}
- @Suppress("DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_SELECT_IMAGE && resultCode == Activity.RESULT_OK && data != null) {
- val media: Media = requireNotNull(data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA))
+ val media: Media = requireNotNull(data.getParcelableExtraCompat(AvatarSelectionActivity.EXTRA_MEDIA, Media::class.java))
viewModel.onAvatarPhotoSelectionCompleted(media)
} else {
super.onActivityResult(requestCode, resultCode, data)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/text/TextAvatarCreationState.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/text/TextAvatarCreationState.kt
index 52493ad3a7..dd8a02f15c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/avatar/text/TextAvatarCreationState.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/text/TextAvatarCreationState.kt
@@ -5,7 +5,7 @@ import org.thoughtcrime.securesms.avatar.AvatarColorItem
import org.thoughtcrime.securesms.avatar.Avatars
data class TextAvatarCreationState(
- val currentAvatar: Avatar.Text,
+ val currentAvatar: Avatar.Text
) {
fun colors(): List = Avatars.colors.map { AvatarColorItem(it, currentAvatar.color == it) }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/vector/VectorAvatarCreationState.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/vector/VectorAvatarCreationState.kt
index 20da33108f..1cb6c1269e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/avatar/vector/VectorAvatarCreationState.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/vector/VectorAvatarCreationState.kt
@@ -5,7 +5,7 @@ import org.thoughtcrime.securesms.avatar.AvatarColorItem
import org.thoughtcrime.securesms.avatar.Avatars
data class VectorAvatarCreationState(
- val currentAvatar: Avatar.Vector,
+ val currentAvatar: Avatar.Vector
) {
fun colors(): List = Avatars.colors.map { AvatarColorItem(it, currentAvatar.color == it) }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupRecordInputStream.java b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupRecordInputStream.java
index 9c11e14e1e..e90f3169b0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupRecordInputStream.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupRecordInputStream.java
@@ -6,6 +6,8 @@
import org.signal.core.util.StreamUtil;
import org.signal.libsignal.protocol.kdf.HKDF;
import org.signal.libsignal.protocol.util.ByteUtil;
+import org.thoughtcrime.securesms.backup.proto.BackupFrame;
+import org.thoughtcrime.securesms.backup.proto.Header;
import java.io.IOException;
import java.io.InputStream;
@@ -45,21 +47,21 @@ class BackupRecordInputStream extends FullBackupBase.BackupStream {
byte[] headerFrame = new byte[headerLength];
StreamUtil.readFully(in, headerFrame);
- BackupProtos.BackupFrame frame = BackupProtos.BackupFrame.parseFrom(headerFrame);
+ BackupFrame frame = BackupFrame.ADAPTER.decode(headerFrame);
- if (!frame.hasHeader()) {
+ if (frame.header_ == null) {
throw new IOException("Backup stream does not start with header!");
}
- BackupProtos.Header header = frame.getHeader();
+ Header header = frame.header_;
- this.iv = header.getIv().toByteArray();
+ this.iv = header.iv.toByteArray();
if (iv.length != 16) {
throw new IOException("Invalid IV length!");
}
- byte[] key = getBackupKey(passphrase, header.hasSalt() ? header.getSalt().toByteArray() : null);
+ byte[] key = getBackupKey(passphrase, header.salt != null ? header.salt.toByteArray() : null);
byte[] derived = HKDF.deriveSecrets(key, "Backup Export".getBytes(), 64);
byte[][] split = ByteUtil.split(derived, 32, 32);
@@ -76,7 +78,7 @@ class BackupRecordInputStream extends FullBackupBase.BackupStream {
}
}
- BackupProtos.BackupFrame readFrame() throws IOException {
+ BackupFrame readFrame() throws IOException {
return readFrame(in);
}
@@ -128,7 +130,7 @@ void readAttachmentTo(OutputStream out, int length) throws IOException {
}
}
- private BackupProtos.BackupFrame readFrame(InputStream in) throws IOException {
+ private BackupFrame readFrame(InputStream in) throws IOException {
try {
byte[] length = new byte[4];
StreamUtil.readFully(in, length);
@@ -151,7 +153,7 @@ private BackupProtos.BackupFrame readFrame(InputStream in) throws IOException {
byte[] plaintext = cipher.doFinal(frame, 0, frame.length - 10);
- return BackupProtos.BackupFrame.parseFrom(plaintext);
+ return BackupFrame.ADAPTER.decode(plaintext);
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupVerifier.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupVerifier.kt
index 0bcc2d59ca..73073ac01e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupVerifier.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupVerifier.kt
@@ -2,7 +2,10 @@ package org.thoughtcrime.securesms.backup
import org.greenrobot.eventbus.EventBus
import org.signal.core.util.logging.Log
-import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame
+import org.thoughtcrime.securesms.backup.proto.Attachment
+import org.thoughtcrime.securesms.backup.proto.Avatar
+import org.thoughtcrime.securesms.backup.proto.BackupFrame
+import org.thoughtcrime.securesms.backup.proto.Sticker
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
@@ -23,11 +26,11 @@ object BackupVerifier {
var frame: BackupFrame = inputStream.readFrame()
cipherStream.use {
- while (!frame.end && !cancellationSignal.isCanceled) {
+ while (frame.end != true && !cancellationSignal.isCanceled) {
val verified = when {
- frame.hasAttachment() -> verifyAttachment(frame.attachment, inputStream)
- frame.hasSticker() -> verifySticker(frame.sticker, inputStream)
- frame.hasAvatar() -> verifyAvatar(frame.avatar, inputStream)
+ frame.attachment != null -> verifyAttachment(frame.attachment!!, inputStream)
+ frame.sticker != null -> verifySticker(frame.sticker!!, inputStream)
+ frame.avatar != null -> verifyAvatar(frame.avatar!!, inputStream)
else -> true
}
@@ -48,9 +51,9 @@ object BackupVerifier {
return true
}
- private fun verifyAttachment(attachment: BackupProtos.Attachment, inputStream: BackupRecordInputStream): Boolean {
+ private fun verifyAttachment(attachment: Attachment, inputStream: BackupRecordInputStream): Boolean {
try {
- inputStream.readAttachmentTo(NullOutputStream, attachment.length)
+ inputStream.readAttachmentTo(NullOutputStream, attachment.length ?: 0)
} catch (e: IOException) {
Log.w(TAG, "Bad attachment id: ${attachment.attachmentId} len: ${attachment.length}", e)
return false
@@ -59,9 +62,9 @@ object BackupVerifier {
return true
}
- private fun verifySticker(sticker: BackupProtos.Sticker, inputStream: BackupRecordInputStream): Boolean {
+ private fun verifySticker(sticker: Sticker, inputStream: BackupRecordInputStream): Boolean {
try {
- inputStream.readAttachmentTo(NullOutputStream, sticker.length)
+ inputStream.readAttachmentTo(NullOutputStream, sticker.length ?: 0)
} catch (e: IOException) {
Log.w(TAG, "Bad sticker id: ${sticker.rowId} len: ${sticker.length}", e)
return false
@@ -69,9 +72,9 @@ object BackupVerifier {
return true
}
- private fun verifyAvatar(avatar: BackupProtos.Avatar, inputStream: BackupRecordInputStream): Boolean {
+ private fun verifyAvatar(avatar: Avatar, inputStream: BackupRecordInputStream): Boolean {
try {
- inputStream.readAttachmentTo(NullOutputStream, avatar.length)
+ inputStream.readAttachmentTo(NullOutputStream, avatar.length ?: 0)
} catch (e: IOException) {
Log.w(TAG, "Bad avatar id: ${avatar.recipientId} len: ${avatar.length}", e)
return false
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java
index ff6881f2f0..c1f8473ff6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java
@@ -12,7 +12,6 @@
import androidx.documentfile.provider.DocumentFile;
import com.annimon.stream.function.Predicate;
-import com.google.protobuf.ByteString;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
@@ -26,6 +25,15 @@
import org.signal.libsignal.protocol.kdf.HKDF;
import org.signal.libsignal.protocol.util.ByteUtil;
import org.thoughtcrime.securesms.attachments.AttachmentId;
+import org.thoughtcrime.securesms.backup.proto.Attachment;
+import org.thoughtcrime.securesms.backup.proto.Avatar;
+import org.thoughtcrime.securesms.backup.proto.BackupFrame;
+import org.thoughtcrime.securesms.backup.proto.DatabaseVersion;
+import org.thoughtcrime.securesms.backup.proto.Header;
+import org.thoughtcrime.securesms.backup.proto.KeyValue;
+import org.thoughtcrime.securesms.backup.proto.SharedPreference;
+import org.thoughtcrime.securesms.backup.proto.SqlStatement;
+import org.thoughtcrime.securesms.backup.proto.Sticker;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.database.AttachmentTable;
@@ -80,6 +88,8 @@
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
+import okio.ByteString;
+
public class FullBackupExporter extends FullBackupBase {
private static final String TAG = Log.tag(FullBackupExporter.class);
@@ -187,7 +197,7 @@ private static BackupEvent internalExport(@NonNull Context context,
stopwatch.split("table::" + table);
}
- for (BackupProtos.SharedPreference preference : TextSecurePreferences.getPreferencesToSaveToBackup(context)) {
+ for (SharedPreference preference : TextSecurePreferences.getPreferencesToSaveToBackup(context)) {
throwIfCanceled(cancellationSignal);
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
outputStream.write(preference);
@@ -287,7 +297,7 @@ private static List exportSchema(@NonNull SQLiteDatabase input, @NonNull
String statement = createStatementsByTable.get(table);
if (statement != null) {
- outputStream.write(BackupProtos.SqlStatement.newBuilder().setStatement(statement).build());
+ outputStream.write(new SqlStatement.Builder().statement(statement).build());
} else {
throw new IOException("Failed to find a create statement for table: " + table);
}
@@ -299,7 +309,7 @@ private static List exportSchema(@NonNull SQLiteDatabase input, @NonNull
String name = cursor.getString(1);
if (isTableAllowed(name)) {
- outputStream.write(BackupProtos.SqlStatement.newBuilder().setStatement(sql).build());
+ outputStream.write(new SqlStatement.Builder().statement(sql).build());
}
}
}
@@ -393,8 +403,10 @@ private static int exportTable(@NonNull String table,
throwIfCanceled(cancellationSignal);
if (predicate == null || predicate.test(cursor)) {
- StringBuilder statement = new StringBuilder(template);
- BackupProtos.SqlStatement.Builder statementBuilder = BackupProtos.SqlStatement.newBuilder();
+ StringBuilder statement = new StringBuilder(template);
+ SqlStatement.Builder statementBuilder = new SqlStatement.Builder();
+
+ statementBuilder.parameters = new ArrayList<>();
statement.append('(');
@@ -402,15 +414,15 @@ private static int exportTable(@NonNull String table,
statement.append('?');
if (cursor.getType(i) == Cursor.FIELD_TYPE_STRING) {
- statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setStringParamter(cursor.getString(i)));
+ statementBuilder.parameters.add(new SqlStatement.SqlParameter.Builder().stringParamter(cursor.getString(i)).build());
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_FLOAT) {
- statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setDoubleParameter(cursor.getDouble(i)));
+ statementBuilder.parameters.add(new SqlStatement.SqlParameter.Builder().doubleParameter(cursor.getDouble(i)).build());
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_INTEGER) {
- statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setIntegerParameter(cursor.getLong(i)));
+ statementBuilder.parameters.add(new SqlStatement.SqlParameter.Builder().integerParameter(cursor.getLong(i)).build());
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_BLOB) {
- statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setBlobParameter(ByteString.copyFrom(cursor.getBlob(i))));
+ statementBuilder.parameters.add(new SqlStatement.SqlParameter.Builder().blobParameter(new ByteString(cursor.getBlob(i))).build());
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_NULL) {
- statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setNullparameter(true));
+ statementBuilder.parameters.add(new SqlStatement.SqlParameter.Builder().nullparameter(true).build());
} else {
throw new AssertionError("unknown type?" + cursor.getType(i));
}
@@ -423,7 +435,7 @@ private static int exportTable(@NonNull String table,
statement.append(')');
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
- outputStream.write(statementBuilder.setStatement(statement.toString()).build());
+ outputStream.write(statementBuilder.statement(statement.toString()).build());
if (postProcess != null) {
count = postProcess.postProcess(cursor, count);
@@ -504,29 +516,30 @@ private static int exportKeyValues(@NonNull BackupFrameOutputStream outputStream
if (!dataSet.containsKey(key)) {
continue;
}
- BackupProtos.KeyValue.Builder builder = BackupProtos.KeyValue.newBuilder()
- .setKey(key);
+
+ KeyValue.Builder builder = new KeyValue.Builder()
+ .key(key);
Class> type = dataSet.getType(key);
if (type == byte[].class) {
byte[] data = dataSet.getBlob(key, null);
if (data != null) {
- builder.setBlobValue(ByteString.copyFrom(dataSet.getBlob(key, null)));
+ builder.blobValue(new ByteString(dataSet.getBlob(key, null)));
} else {
Log.w(TAG, "Skipping storing null blob for key: " + key);
}
} else if (type == Boolean.class) {
- builder.setBooleanValue(dataSet.getBoolean(key, false));
+ builder.booleanValue(dataSet.getBoolean(key, false));
} else if (type == Float.class) {
- builder.setFloatValue(dataSet.getFloat(key, 0));
+ builder.floatValue(dataSet.getFloat(key, 0));
} else if (type == Integer.class) {
- builder.setIntegerValue(dataSet.getInteger(key, 0));
+ builder.integerValue(dataSet.getInteger(key, 0));
} else if (type == Long.class) {
- builder.setLongValue(dataSet.getLong(key, 0));
+ builder.longValue(dataSet.getLong(key, 0));
} else if (type == String.class) {
String data = dataSet.getString(key, null);
if (data != null) {
- builder.setStringValue(dataSet.getString(key, null));
+ builder.stringValue(dataSet.getString(key, null));
} else {
Log.w(TAG, "Skipping storing null string for key: " + key);
}
@@ -596,10 +609,12 @@ private BackupFrameOutputStream(@NonNull OutputStream output, @NonNull String pa
mac.init(new SecretKeySpec(macKey, "HmacSHA256"));
- byte[] header = BackupProtos.BackupFrame.newBuilder().setHeader(BackupProtos.Header.newBuilder()
- .setIv(ByteString.copyFrom(iv))
- .setSalt(ByteString.copyFrom(salt)))
- .build().toByteArray();
+ byte[] header = new BackupFrame.Builder().header_(new Header.Builder()
+ .iv(new okio.ByteString(iv))
+ .salt(new okio.ByteString(salt))
+ .build())
+ .build()
+ .encode();
outputStream.write(Conversions.intToByteArray(header.length));
outputStream.write(header);
@@ -608,26 +623,26 @@ private BackupFrameOutputStream(@NonNull OutputStream output, @NonNull String pa
}
}
- public void write(BackupProtos.SharedPreference preference) throws IOException {
- write(outputStream, BackupProtos.BackupFrame.newBuilder().setPreference(preference).build());
+ public void write(SharedPreference preference) throws IOException {
+ write(outputStream, new BackupFrame.Builder().preference(preference).build());
}
- public void write(BackupProtos.KeyValue keyValue) throws IOException {
- write(outputStream, BackupProtos.BackupFrame.newBuilder().setKeyValue(keyValue).build());
+ public void write(KeyValue keyValue) throws IOException {
+ write(outputStream, new BackupFrame.Builder().keyValue(keyValue).build());
}
- public void write(BackupProtos.SqlStatement statement) throws IOException {
- write(outputStream, BackupProtos.BackupFrame.newBuilder().setStatement(statement).build());
+ public void write(SqlStatement statement) throws IOException {
+ write(outputStream, new BackupFrame.Builder().statement(statement).build());
}
public void write(@NonNull String avatarName, @NonNull InputStream in, long size) throws IOException {
try {
- write(outputStream, BackupProtos.BackupFrame.newBuilder()
- .setAvatar(BackupProtos.Avatar.newBuilder()
- .setRecipientId(avatarName)
- .setLength(Util.toIntExact(size))
- .build())
- .build());
+ write(outputStream, new BackupFrame.Builder()
+ .avatar(new Avatar.Builder()
+ .recipientId(avatarName)
+ .length(Util.toIntExact(size))
+ .build())
+ .build());
} catch (ArithmeticException e) {
Log.w(TAG, "Unable to write avatar to backup", e);
throw new InvalidBackupStreamException();
@@ -640,13 +655,13 @@ public void write(@NonNull String avatarName, @NonNull InputStream in, long size
public void write(@NonNull AttachmentId attachmentId, @NonNull InputStream in, long size) throws IOException {
try {
- write(outputStream, BackupProtos.BackupFrame.newBuilder()
- .setAttachment(BackupProtos.Attachment.newBuilder()
- .setRowId(attachmentId.getRowId())
- .setAttachmentId(attachmentId.getUniqueId())
- .setLength(Util.toIntExact(size))
- .build())
- .build());
+ write(outputStream, new BackupFrame.Builder()
+ .attachment(new Attachment.Builder()
+ .rowId(attachmentId.getRowId())
+ .attachmentId(attachmentId.getUniqueId())
+ .length(Util.toIntExact(size))
+ .build())
+ .build());
} catch (ArithmeticException e) {
Log.w(TAG, "Unable to write " + attachmentId + " to backup", e);
throw new InvalidBackupStreamException();
@@ -654,22 +669,20 @@ public void write(@NonNull AttachmentId attachmentId, @NonNull InputStream in, l
long totalWritten = writeStream(in);
if (totalWritten != size) {
- if (totalWritten == 0) {
- // MOLLY: Quick workaround for zero-sized broken attachments
- SignalDatabase.attachments().deleteAttachment(attachmentId);
- }
+ // MOLLY: Quick workaround for broken attachments
+ SignalDatabase.attachments().deleteAttachment(attachmentId);
throw new IOException("Size mismatch!");
}
}
public void writeSticker(long rowId, @NonNull InputStream in, long size) throws IOException {
try {
- write(outputStream, BackupProtos.BackupFrame.newBuilder()
- .setSticker(BackupProtos.Sticker.newBuilder()
- .setRowId(rowId)
- .setLength(Util.toIntExact(size))
- .build())
- .build());
+ write(outputStream, new BackupFrame.Builder()
+ .sticker(new Sticker.Builder()
+ .rowId(rowId)
+ .length(Util.toIntExact(size))
+ .build())
+ .build());
} catch (ArithmeticException e) {
Log.w(TAG, "Unable to write sticker to backup", e);
throw new InvalidBackupStreamException();
@@ -681,13 +694,13 @@ public void writeSticker(long rowId, @NonNull InputStream in, long size) throws
}
void writeDatabaseVersion(int version) throws IOException {
- write(outputStream, BackupProtos.BackupFrame.newBuilder()
- .setVersion(BackupProtos.DatabaseVersion.newBuilder().setVersion(version))
- .build());
+ write(outputStream, new BackupFrame.Builder()
+ .version(new DatabaseVersion.Builder().version(version).build())
+ .build());
}
void writeEnd() throws IOException {
- write(outputStream, BackupProtos.BackupFrame.newBuilder().setEnd(true).build());
+ write(outputStream, new BackupFrame.Builder().end(true).build());
}
/**
@@ -728,12 +741,12 @@ private long writeStream(@NonNull InputStream inputStream) throws IOException {
}
}
- private void write(@NonNull OutputStream out, @NonNull BackupProtos.BackupFrame frame) throws IOException {
+ private void write(@NonNull OutputStream out, @NonNull BackupFrame frame) throws IOException {
try {
Conversions.intToByteArray(iv, 0, counter++);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
- byte[] frameCiphertext = cipher.doFinal(frame.toByteArray());
+ byte[] frameCiphertext = cipher.doFinal(frame.encode());
byte[] frameMac = mac.doFinal(frameCiphertext);
byte[] length = Conversions.intToByteArray(frameCiphertext.length + 10);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java
index 57db1ecced..aecd50ffd9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java
@@ -18,12 +18,14 @@
import org.signal.core.util.SqlUtil;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.BuildConfig;
-import org.thoughtcrime.securesms.backup.BackupProtos.Attachment;
-import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame;
-import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion;
-import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference;
-import org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement;
-import org.thoughtcrime.securesms.backup.BackupProtos.Sticker;
+import org.thoughtcrime.securesms.backup.proto.Attachment;
+import org.thoughtcrime.securesms.backup.proto.Avatar;
+import org.thoughtcrime.securesms.backup.proto.BackupFrame;
+import org.thoughtcrime.securesms.backup.proto.DatabaseVersion;
+import org.thoughtcrime.securesms.backup.proto.KeyValue;
+import org.thoughtcrime.securesms.backup.proto.SharedPreference;
+import org.thoughtcrime.securesms.backup.proto.SqlStatement;
+import org.thoughtcrime.securesms.backup.proto.Sticker;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.EncryptedPreferences;
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
@@ -91,17 +93,17 @@ public static void importFile(@NonNull Context context, @NonNull AttachmentSecre
BackupFrame frame;
- while (!(frame = inputStream.readFrame()).getEnd()) {
+ while ((frame = inputStream.readFrame()).end != Boolean.TRUE) {
if (count % 100 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, count, 0));
count++;
- if (frame.hasVersion()) processVersion(db, frame.getVersion());
- else if (frame.hasStatement()) tryProcessStatement(db, frame.getStatement());
- else if (frame.hasPreference()) processPreference(context, frame.getPreference());
- else if (frame.hasAttachment()) processAttachment(context, attachmentSecret, db, frame.getAttachment(), inputStream);
- else if (frame.hasSticker()) processSticker(context, attachmentSecret, db, frame.getSticker(), inputStream);
- else if (frame.hasAvatar()) processAvatar(context, db, frame.getAvatar(), inputStream);
- else if (frame.hasKeyValue()) processKeyValue(frame.getKeyValue());
+ if (frame.version != null) processVersion(db, frame.version);
+ else if (frame.statement != null) tryProcessStatement(db, frame.statement);
+ else if (frame.preference != null) processPreference(context, frame.preference);
+ else if (frame.attachment != null) processAttachment(context, attachmentSecret, db, frame.attachment, inputStream);
+ else if (frame.sticker != null) processSticker(context, attachmentSecret, db, frame.sticker, inputStream);
+ else if (frame.avatar != null) processAvatar(context, db, frame.avatar, inputStream);
+ else if (frame.keyValue != null) processKeyValue(frame.keyValue);
else count--;
}
@@ -124,11 +126,11 @@ public static void importFile(@NonNull Context context, @NonNull AttachmentSecre
}
private static void processVersion(@NonNull SQLiteDatabase db, DatabaseVersion version) throws IOException {
- if (version.getVersion() > db.getVersion()) {
- throw new DatabaseDowngradeException(db.getVersion(), version.getVersion());
+ if (version.version == null || version.version > db.getVersion()) {
+ throw new DatabaseDowngradeException(db.getVersion(), version.version != null ? version.version : -1);
}
- db.setVersion(version.getVersion());
+ db.setVersion(version.version);
}
private static void tryProcessStatement(@NonNull SQLiteDatabase db, SqlStatement statement) {
@@ -136,9 +138,9 @@ private static void tryProcessStatement(@NonNull SQLiteDatabase db, SqlStatement
processStatement(db, statement);
} catch (SQLiteConstraintException e) {
String tableName = "?";
- String statementString = statement.getStatement();
+ String statementString = statement.statement;
- if (statementString.startsWith("INSERT INTO ")) {
+ if (statementString != null && statementString.startsWith("INSERT INTO ")) {
int nameStart = "INSERT INTO ".length();
int nameEnd = statementString.indexOf(" ", "INSERT INTO ".length());
@@ -157,27 +159,32 @@ private static void tryProcessStatement(@NonNull SQLiteDatabase db, SqlStatement
}
private static void processStatement(@NonNull SQLiteDatabase db, SqlStatement statement) {
- boolean isForMmsFtsSecretTable = statement.getStatement().contains(SearchTable.FTS_TABLE_NAME + "_");
- boolean isForEmojiSecretTable = statement.getStatement().contains(EmojiSearchTable.TABLE_NAME + "_");
- boolean isForSqliteSecretTable = statement.getStatement().toLowerCase().startsWith("create table sqlite_");
+ if (statement.statement == null) {
+ Log.w(TAG, "Null statement!");
+ return;
+ }
+
+ boolean isForMmsFtsSecretTable = statement.statement.contains(SearchTable.FTS_TABLE_NAME + "_");
+ boolean isForEmojiSecretTable = statement.statement.contains(EmojiSearchTable.TABLE_NAME + "_");
+ boolean isForSqliteSecretTable = statement.statement.toLowerCase().startsWith("create table sqlite_");
if (isForMmsFtsSecretTable || isForEmojiSecretTable || isForSqliteSecretTable) {
- Log.i(TAG, "Ignoring import for statement: " + statement.getStatement());
+ Log.i(TAG, "Ignoring import for statement: " + statement.statement);
return;
}
List