From d3ce899a8056c9b9d307f9993845603b0016c058 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 5 May 2023 12:07:19 +0930 Subject: [PATCH 01/13] Synchronize all Cipher#doFinal --- .../securesms/backup/FullBackupExporter.kt | 35 +++++++++------ .../session/libsession/utilities/AESGCM.kt | 17 ++++--- .../session/libsignal/crypto/CipherUtil.java | 5 +++ .../session/libsignal/crypto/DiffieHellman.kt | 45 ------------------- .../session/libsignal/crypto/kdf/HKDF.java | 8 +--- .../streams/AttachmentCipherOutputStream.java | 15 ++++--- .../streams/ProfileCipherInputStream.java | 30 +++++++------ .../streams/ProfileCipherOutputStream.java | 16 ++++--- 8 files changed, 75 insertions(+), 96 deletions(-) create mode 100644 libsignal/src/main/java/org/session/libsignal/crypto/CipherUtil.java delete mode 100644 libsignal/src/main/java/org/session/libsignal/crypto/DiffieHellman.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt index 6b5d47a2e6..783468eee7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt @@ -14,6 +14,7 @@ import org.session.libsession.avatars.AvatarHelper import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId import org.session.libsession.utilities.Conversions import org.session.libsession.utilities.Util +import org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK import org.session.libsignal.crypto.kdf.HKDFv3 import org.session.libsignal.utilities.ByteUtil import org.session.libsignal.utilities.Log @@ -289,7 +290,7 @@ object FullBackupExporter { private var counter: Int = 0 - constructor(outputStream: OutputStream, passphrase: String) : super() { + private constructor(outputStream: OutputStream, passphrase: String) : super() { try { val salt = Util.getSecretBytes(32) val key = BackupUtil.computeBackupKey(passphrase, salt) @@ -381,18 +382,24 @@ object FullBackupExporter { private fun writeStream(inputStream: InputStream) { try { Conversions.intToByteArray(iv, 0, counter++) - cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv)) - mac.update(iv) - val buffer = ByteArray(8192) - var read: Int - while (inputStream.read(buffer).also { read = it } != -1) { - val ciphertext = cipher.update(buffer, 0, read) - if (ciphertext != null) { - outputStream.write(ciphertext) - mac.update(ciphertext) + val remainder = synchronized(CIPHER_LOCK) { + cipher.init( + Cipher.ENCRYPT_MODE, + SecretKeySpec(cipherKey, "AES"), + IvParameterSpec(iv) + ) + mac.update(iv) + val buffer = ByteArray(8192) + var read: Int + while (inputStream.read(buffer).also { read = it } != -1) { + val ciphertext = cipher.update(buffer, 0, read) + if (ciphertext != null) { + outputStream.write(ciphertext) + mac.update(ciphertext) + } } + cipher.doFinal() } - val remainder = cipher.doFinal() outputStream.write(remainder) mac.update(remainder) val attachmentDigest = mac.doFinal() @@ -414,8 +421,10 @@ object FullBackupExporter { private fun write(out: OutputStream, frame: BackupFrame) { try { Conversions.intToByteArray(iv, 0, counter++) - cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv)) - val frameCiphertext = cipher.doFinal(frame.toByteArray()) + val frameCiphertext = synchronized(CIPHER_LOCK) { + cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv)) + cipher.doFinal(frame.toByteArray()) + } val frameMac = mac.doFinal(frameCiphertext) val length = Conversions.intToByteArray(frameCiphertext.size + 10) out.write(length) diff --git a/libsession/src/main/java/org/session/libsession/utilities/AESGCM.kt b/libsession/src/main/java/org/session/libsession/utilities/AESGCM.kt index 225900b096..4a6a588dc2 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/AESGCM.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/AESGCM.kt @@ -1,6 +1,7 @@ package org.session.libsession.utilities import androidx.annotation.WorkerThread +import org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK import org.session.libsignal.utilities.ByteUtil import org.session.libsignal.utilities.Util import org.session.libsignal.utilities.Hex @@ -27,9 +28,11 @@ internal object AESGCM { internal fun decrypt(ivAndCiphertext: ByteArray, symmetricKey: ByteArray): ByteArray { val iv = ivAndCiphertext.sliceArray(0 until ivSize) val ciphertext = ivAndCiphertext.sliceArray(ivSize until ivAndCiphertext.count()) - val cipher = Cipher.getInstance("AES/GCM/NoPadding") - cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(symmetricKey, "AES"), GCMParameterSpec(gcmTagSize, iv)) - return cipher.doFinal(ciphertext) + synchronized(CIPHER_LOCK) { + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(symmetricKey, "AES"), GCMParameterSpec(gcmTagSize, iv)) + return cipher.doFinal(ciphertext) + } } /** @@ -47,9 +50,11 @@ internal object AESGCM { */ internal fun encrypt(plaintext: ByteArray, symmetricKey: ByteArray): ByteArray { val iv = Util.getSecretBytes(ivSize) - val cipher = Cipher.getInstance("AES/GCM/NoPadding") - cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(symmetricKey, "AES"), GCMParameterSpec(gcmTagSize, iv)) - return ByteUtil.combine(iv, cipher.doFinal(plaintext)) + synchronized(CIPHER_LOCK) { + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(symmetricKey, "AES"), GCMParameterSpec(gcmTagSize, iv)) + return ByteUtil.combine(iv, cipher.doFinal(plaintext)) + } } /** diff --git a/libsignal/src/main/java/org/session/libsignal/crypto/CipherUtil.java b/libsignal/src/main/java/org/session/libsignal/crypto/CipherUtil.java new file mode 100644 index 0000000000..d3423a0bd4 --- /dev/null +++ b/libsignal/src/main/java/org/session/libsignal/crypto/CipherUtil.java @@ -0,0 +1,5 @@ +package org.session.libsignal.crypto; + +public class CipherUtil { + public static final Object CIPHER_LOCK = new Object(); +} diff --git a/libsignal/src/main/java/org/session/libsignal/crypto/DiffieHellman.kt b/libsignal/src/main/java/org/session/libsignal/crypto/DiffieHellman.kt deleted file mode 100644 index 2b613247bf..0000000000 --- a/libsignal/src/main/java/org/session/libsignal/crypto/DiffieHellman.kt +++ /dev/null @@ -1,45 +0,0 @@ -package org.session.libsignal.crypto - -import org.whispersystems.curve25519.Curve25519 -import org.session.libsignal.utilities.Util -import javax.crypto.Cipher -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec - -object DiffieHellman { - private val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") - private val curve = Curve25519.getInstance(Curve25519.BEST) - private val ivSize = 16 - - @JvmStatic @Throws - fun encrypt(plaintext: ByteArray, symmetricKey: ByteArray): ByteArray { - val iv = Util.getSecretBytes(ivSize) - val ivSpec = IvParameterSpec(iv) - val secretKeySpec = SecretKeySpec(symmetricKey, "AES") - cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivSpec) - val ciphertext = cipher.doFinal(plaintext) - return iv + ciphertext - } - - @JvmStatic @Throws - fun encrypt(plaintext: ByteArray, publicKey: ByteArray, privateKey: ByteArray): ByteArray { - val symmetricKey = curve.calculateAgreement(publicKey, privateKey) - return encrypt(plaintext, symmetricKey) - } - - @JvmStatic @Throws - fun decrypt(ivAndCiphertext: ByteArray, symmetricKey: ByteArray): ByteArray { - val iv = ivAndCiphertext.sliceArray(0 until ivSize) - val ciphertext = ivAndCiphertext.sliceArray(ivSize until ivAndCiphertext.size) - val ivSpec = IvParameterSpec(iv) - val secretKeySpec = SecretKeySpec(symmetricKey, "AES") - cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivSpec) - return cipher.doFinal(ciphertext) - } - - @JvmStatic @Throws - fun decrypt(ivAndCiphertext: ByteArray, publicKey: ByteArray, privateKey: ByteArray): ByteArray { - val symmetricKey = curve.calculateAgreement(publicKey, privateKey) - return decrypt(ivAndCiphertext, symmetricKey) - } -} diff --git a/libsignal/src/main/java/org/session/libsignal/crypto/kdf/HKDF.java b/libsignal/src/main/java/org/session/libsignal/crypto/kdf/HKDF.java index 3295f06e52..73c87c075d 100644 --- a/libsignal/src/main/java/org/session/libsignal/crypto/kdf/HKDF.java +++ b/libsignal/src/main/java/org/session/libsignal/crypto/kdf/HKDF.java @@ -39,9 +39,7 @@ public abstract class HKDF { Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(salt, "HmacSHA256")); return mac.doFinal(inputKeyMaterial); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); - } catch (InvalidKeyException e) { + } catch (NoSuchAlgorithmException | InvalidKeyException e) { throw new AssertionError(e); } } @@ -73,9 +71,7 @@ public abstract class HKDF { } return results.toByteArray(); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); - } catch (InvalidKeyException e) { + } catch (NoSuchAlgorithmException | InvalidKeyException e) { throw new AssertionError(e); } } diff --git a/libsignal/src/main/java/org/session/libsignal/streams/AttachmentCipherOutputStream.java b/libsignal/src/main/java/org/session/libsignal/streams/AttachmentCipherOutputStream.java index 91c3563700..2f58c84c78 100644 --- a/libsignal/src/main/java/org/session/libsignal/streams/AttachmentCipherOutputStream.java +++ b/libsignal/src/main/java/org/session/libsignal/streams/AttachmentCipherOutputStream.java @@ -6,6 +6,8 @@ package org.session.libsignal.streams; +import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK; + import org.session.libsignal.utilities.Util; import java.io.IOException; @@ -68,16 +70,17 @@ public class AttachmentCipherOutputStream extends DigestingOutputStream { @Override public void flush() throws IOException { try { - byte[] ciphertext = cipher.doFinal(); + byte[] ciphertext; + synchronized (CIPHER_LOCK) { + ciphertext = cipher.doFinal(); + } byte[] auth = mac.doFinal(ciphertext); super.write(ciphertext); super.write(auth); super.flush(); - } catch (IllegalBlockSizeException e) { - throw new AssertionError(e); - } catch (BadPaddingException e) { + } catch (IllegalBlockSizeException | BadPaddingException e) { throw new AssertionError(e); } } @@ -97,9 +100,7 @@ public class AttachmentCipherOutputStream extends DigestingOutputStream { private Cipher initializeCipher() { try { return Cipher.getInstance("AES/CBC/PKCS5Padding"); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); - } catch (NoSuchPaddingException e) { + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { throw new AssertionError(e); } } diff --git a/libsignal/src/main/java/org/session/libsignal/streams/ProfileCipherInputStream.java b/libsignal/src/main/java/org/session/libsignal/streams/ProfileCipherInputStream.java index aa15eb00c6..19996c17cb 100644 --- a/libsignal/src/main/java/org/session/libsignal/streams/ProfileCipherInputStream.java +++ b/libsignal/src/main/java/org/session/libsignal/streams/ProfileCipherInputStream.java @@ -1,5 +1,7 @@ package org.session.libsignal.streams; +import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK; + import org.session.libsignal.utilities.Util; import java.io.FilterInputStream; @@ -62,23 +64,23 @@ public class ProfileCipherInputStream extends FilterInputStream { byte[] ciphertext = new byte[outputLength / 2]; int read = in.read(ciphertext, 0, ciphertext.length); - if (read == -1) { - if (cipher.getOutputSize(0) > outputLength) { - throw new AssertionError("Need: " + cipher.getOutputSize(0) + " but only have: " + outputLength); - } + synchronized (CIPHER_LOCK) { + if (read == -1) { + if (cipher.getOutputSize(0) > outputLength) { + throw new AssertionError("Need: " + cipher.getOutputSize(0) + " but only have: " + outputLength); + } - finished = true; - return cipher.doFinal(output, outputOffset); - } else { - if (cipher.getOutputSize(read) > outputLength) { - throw new AssertionError("Need: " + cipher.getOutputSize(read) + " but only have: " + outputLength); - } + finished = true; + return cipher.doFinal(output, outputOffset); + } else { + if (cipher.getOutputSize(read) > outputLength) { + throw new AssertionError("Need: " + cipher.getOutputSize(read) + " but only have: " + outputLength); + } - return cipher.update(ciphertext, 0, read, output, outputOffset); + return cipher.update(ciphertext, 0, read, output, outputOffset); + } } - } catch (IllegalBlockSizeException e) { - throw new AssertionError(e); - } catch(ShortBufferException e) { + } catch (IllegalBlockSizeException | ShortBufferException e) { throw new AssertionError(e); } catch (BadPaddingException e) { throw new IOException(e); diff --git a/libsignal/src/main/java/org/session/libsignal/streams/ProfileCipherOutputStream.java b/libsignal/src/main/java/org/session/libsignal/streams/ProfileCipherOutputStream.java index 9d4e13a0c2..f47a5f72b6 100644 --- a/libsignal/src/main/java/org/session/libsignal/streams/ProfileCipherOutputStream.java +++ b/libsignal/src/main/java/org/session/libsignal/streams/ProfileCipherOutputStream.java @@ -1,5 +1,7 @@ package org.session.libsignal.streams; +import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK; + import java.io.IOException; import java.io.OutputStream; import java.security.InvalidAlgorithmParameterException; @@ -54,20 +56,24 @@ public class ProfileCipherOutputStream extends DigestingOutputStream { byte[] input = new byte[1]; input[0] = (byte)b; - byte[] output = cipher.update(input); + byte[] output; + synchronized (CIPHER_LOCK) { + output = cipher.update(input); + } super.write(output); } @Override public void flush() throws IOException { try { - byte[] output = cipher.doFinal(); + byte[] output; + synchronized (CIPHER_LOCK) { + output = cipher.doFinal(); + } super.write(output); super.flush(); - } catch (BadPaddingException e) { - throw new AssertionError(e); - } catch (IllegalBlockSizeException e) { + } catch (BadPaddingException | IllegalBlockSizeException e) { throw new AssertionError(e); } } From a9078c8d08939604087b21c73446ba66bfe3248d Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 5 May 2023 12:32:54 +0930 Subject: [PATCH 02/13] ...and the rest --- .../securesms/backup/FullBackupImporter.kt | 39 ++++++++++++------- .../securesms/crypto/KeyStoreHelper.java | 30 +++++++------- .../securesms/logging/LogFile.java | 27 +++++++------ .../streams/AttachmentCipherInputStream.java | 35 +++++++---------- 4 files changed, 68 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.kt index b40c049bc2..7d2daa910e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.kt @@ -12,6 +12,7 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.utilities.Address import org.session.libsession.utilities.Conversions import org.session.libsession.utilities.Util +import org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK import org.session.libsignal.crypto.kdf.HKDFv3 import org.session.libsignal.utilities.ByteUtil import org.session.libsignal.utilities.Log @@ -243,7 +244,7 @@ object FullBackupImporter { val split = ByteUtil.split(derived, 32, 32) cipherKey = split[0] macKey = split[1] - cipher = Cipher.getInstance("AES/CTR/NoPadding") + cipher = synchronized(CIPHER_LOCK) { Cipher.getInstance("AES/CTR/NoPadding") } mac = Mac.getInstance("HmacSHA256") mac.init(SecretKeySpec(macKey, "HmacSHA256")) counter = Conversions.byteArrayToInt(iv) @@ -269,20 +270,26 @@ object FullBackupImporter { var length = length try { Conversions.intToByteArray(iv, 0, counter++) - cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv)) - mac.update(iv) - val buffer = ByteArray(8192) - while (length > 0) { - val read = inputStream.read(buffer, 0, Math.min(buffer.size, length)) - if (read == -1) throw IOException("File ended early!") - mac.update(buffer, 0, read) - val plaintext = cipher.update(buffer, 0, read) - if (plaintext != null) { - out.write(plaintext, 0, plaintext.size) + val plaintext = synchronized(CIPHER_LOCK) { + cipher.init( + Cipher.DECRYPT_MODE, + SecretKeySpec(cipherKey, "AES"), + IvParameterSpec(iv) + ) + mac.update(iv) + val buffer = ByteArray(8192) + while (length > 0) { + val read = inputStream.read(buffer, 0, Math.min(buffer.size, length)) + if (read == -1) throw IOException("File ended early!") + mac.update(buffer, 0, read) + val plaintext = cipher.update(buffer, 0, read) + if (plaintext != null) { + out.write(plaintext, 0, plaintext.size) + } + length -= read } - length -= read + cipher.doFinal() } - val plaintext = cipher.doFinal() if (plaintext != null) { out.write(plaintext, 0, plaintext.size) } @@ -325,8 +332,10 @@ object FullBackupImporter { throw IOException("Bad MAC") } Conversions.intToByteArray(iv, 0, counter++) - cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv)) - val plaintext = cipher.doFinal(frame, 0, frame.size - 10) + val plaintext = synchronized(CIPHER_LOCK) { + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv)) + cipher.doFinal(frame, 0, frame.size - 10) + } BackupFrame.parseFrom(plaintext) } catch (e: Exception) { when (e) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java index 43e9865598..c0372cc7f1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java @@ -1,6 +1,8 @@ package org.thoughtcrime.securesms.crypto; +import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK; + import android.os.Build; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyProperties; @@ -45,44 +47,44 @@ public final class KeyStoreHelper { private static final String ANDROID_KEY_STORE = "AndroidKeyStore"; private static final String KEY_ALIAS = "SignalSecret"; - @RequiresApi(Build.VERSION_CODES.M) public static SealedData seal(@NonNull byte[] input) { SecretKey secretKey = getOrCreateKeyStoreEntry(); try { - Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); - cipher.init(Cipher.ENCRYPT_MODE, secretKey); + synchronized (CIPHER_LOCK) { + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); - byte[] iv = cipher.getIV(); - byte[] data = cipher.doFinal(input); + byte[] iv = cipher.getIV(); + byte[] data = cipher.doFinal(input); - return new SealedData(iv, data); + return new SealedData(iv, data); + } } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { throw new AssertionError(e); } } - @RequiresApi(Build.VERSION_CODES.M) public static byte[] unseal(@NonNull SealedData sealedData) { SecretKey secretKey = getKeyStoreEntry(); try { - Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); - cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, sealedData.iv)); + synchronized (CIPHER_LOCK) { + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, sealedData.iv)); - return cipher.doFinal(sealedData.data); + return cipher.doFinal(sealedData.data); + } } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) { throw new AssertionError(e); } } - @RequiresApi(Build.VERSION_CODES.M) private static SecretKey getOrCreateKeyStoreEntry() { if (hasKeyStoreEntry()) return getKeyStoreEntry(); else return createKeyStoreEntry(); } - @RequiresApi(Build.VERSION_CODES.M) private static SecretKey createKeyStoreEntry() { try { KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE); @@ -99,7 +101,6 @@ public final class KeyStoreHelper { } } - @RequiresApi(Build.VERSION_CODES.M) private static SecretKey getKeyStoreEntry() { KeyStore keyStore = getKeyStore(); @@ -137,7 +138,6 @@ public final class KeyStoreHelper { } } - @RequiresApi(Build.VERSION_CODES.M) private static boolean hasKeyStoreEntry() { try { KeyStore ks = KeyStore.getInstance(ANDROID_KEY_STORE); @@ -202,7 +202,5 @@ public final class KeyStoreHelper { return Base64.decode(p.getValueAsString(), Base64.NO_WRAP | Base64.NO_PADDING); } } - } - } diff --git a/app/src/main/java/org/thoughtcrime/securesms/logging/LogFile.java b/app/src/main/java/org/thoughtcrime/securesms/logging/LogFile.java index f0c083ca1d..909f19e08c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logging/LogFile.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logging/LogFile.java @@ -1,5 +1,7 @@ package org.thoughtcrime.securesms.logging; +import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK; + import androidx.annotation.NonNull; import org.session.libsession.utilities.Conversions; @@ -66,15 +68,17 @@ class LogFile { byte[] plaintext = entry.getBytes(); try { - cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secret, "AES"), new IvParameterSpec(ivBuffer)); + synchronized (CIPHER_LOCK) { + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secret, "AES"), new IvParameterSpec(ivBuffer)); - int cipherLength = cipher.getOutputSize(plaintext.length); - byte[] ciphertext = ciphertextBuffer.get(cipherLength); - cipherLength = cipher.doFinal(plaintext, 0, plaintext.length, ciphertext); + int cipherLength = cipher.getOutputSize(plaintext.length); + byte[] ciphertext = ciphertextBuffer.get(cipherLength); + cipherLength = cipher.doFinal(plaintext, 0, plaintext.length, ciphertext); - outputStream.write(ivBuffer); - outputStream.write(Conversions.intToByteArray(cipherLength)); - outputStream.write(ciphertext, 0, cipherLength); + outputStream.write(ivBuffer); + outputStream.write(Conversions.intToByteArray(cipherLength)); + outputStream.write(ciphertext, 0, cipherLength); + } outputStream.flush(); } catch (ShortBufferException | InvalidAlgorithmParameterException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) { @@ -134,10 +138,11 @@ class LogFile { Util.readFully(inputStream, ciphertext, length); try { - cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secret, "AES"), new IvParameterSpec(ivBuffer)); - byte[] plaintext = cipher.doFinal(ciphertext, 0, length); - - return new String(plaintext); + synchronized (CIPHER_LOCK) { + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secret, "AES"), new IvParameterSpec(ivBuffer)); + byte[] plaintext = cipher.doFinal(ciphertext, 0, length); + return new String(plaintext); + } } catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) { throw new AssertionError(e); } diff --git a/libsignal/src/main/java/org/session/libsignal/streams/AttachmentCipherInputStream.java b/libsignal/src/main/java/org/session/libsignal/streams/AttachmentCipherInputStream.java index 3158d35f73..fd3c8123df 100644 --- a/libsignal/src/main/java/org/session/libsignal/streams/AttachmentCipherInputStream.java +++ b/libsignal/src/main/java/org/session/libsignal/streams/AttachmentCipherInputStream.java @@ -6,6 +6,8 @@ package org.session.libsignal.streams; +import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK; + import org.session.libsignal.exceptions.InvalidMacException; import org.session.libsignal.exceptions.InvalidMessageException; import org.session.libsignal.utilities.Util; @@ -92,19 +94,15 @@ public class AttachmentCipherInputStream extends FilterInputStream { byte[] iv = new byte[BLOCK_SIZE]; readFully(iv); - this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv)); + synchronized (CIPHER_LOCK) { + this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv)); + } this.done = false; this.totalRead = 0; this.totalDataSize = totalDataSize; - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); - } catch (InvalidKeyException e) { - throw new AssertionError(e); - } catch (NoSuchPaddingException e) { - throw new AssertionError(e); - } catch (InvalidAlgorithmParameterException e) { + } catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException e) { throw new AssertionError(e); } } @@ -141,15 +139,12 @@ public class AttachmentCipherInputStream extends FilterInputStream { private int readFinal(byte[] buffer, int offset, int length) throws IOException { try { - int flourish = cipher.doFinal(buffer, offset); - - done = true; - return flourish; - } catch (IllegalBlockSizeException e) { - throw new IOException(e); - } catch (BadPaddingException e) { - throw new IOException(e); - } catch (ShortBufferException e) { + synchronized (CIPHER_LOCK) { + int flourish = cipher.doFinal(buffer, offset); + done = true; + return flourish; + } + } catch (IllegalBlockSizeException | ShortBufferException | BadPaddingException e) { throw new IOException(e); } } @@ -234,9 +229,7 @@ public class AttachmentCipherInputStream extends FilterInputStream { throw new InvalidMacException("Digest doesn't match!"); } - } catch (IOException e) { - throw new InvalidMacException(e); - } catch (ArithmeticException e) { + } catch (IOException | ArithmeticException e) { throw new InvalidMacException(e); } catch (NoSuchAlgorithmException e) { throw new AssertionError(e); From 9e6d1e27fc4657170e8dd7c87b64f618f3bd16ff Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 5 May 2023 12:33:50 +0930 Subject: [PATCH 03/13] Add comment --- .../src/main/java/org/session/libsignal/crypto/CipherUtil.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libsignal/src/main/java/org/session/libsignal/crypto/CipherUtil.java b/libsignal/src/main/java/org/session/libsignal/crypto/CipherUtil.java index d3423a0bd4..a6a3808bb4 100644 --- a/libsignal/src/main/java/org/session/libsignal/crypto/CipherUtil.java +++ b/libsignal/src/main/java/org/session/libsignal/crypto/CipherUtil.java @@ -1,5 +1,8 @@ package org.session.libsignal.crypto; public class CipherUtil { + // Cipher operations are not thread-safe so we synchronize over them through doFinal to + // prevent crashes with quickly repeated encrypt/decrypt operations + // https://github.com/mozilla-mobile/android-components/issues/5342 public static final Object CIPHER_LOCK = new Object(); } From 6a5d97a0f063f7385f61d18202352e98ad968cf0 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 5 May 2023 12:37:46 +0930 Subject: [PATCH 04/13] Fix something --- .../org/thoughtcrime/securesms/backup/FullBackupExporter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt index 783468eee7..5eba9b9945 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt @@ -279,7 +279,7 @@ object FullBackupExporter { return false } - private class BackupFrameOutputStream : Closeable, Flushable { + private class BackupFrameOutputStream(outputStream: OutputStream, passphrase: String) : Closeable, Flushable { private val outputStream: OutputStream private var cipher: Cipher @@ -290,7 +290,7 @@ object FullBackupExporter { private var counter: Int = 0 - private constructor(outputStream: OutputStream, passphrase: String) : super() { + init { try { val salt = Util.getSecretBytes(32) val key = BackupUtil.computeBackupKey(passphrase, salt) From 30d748e1476a7dc9f7cf5f4ee00d65db22544deb Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 19 May 2023 23:44:07 +0930 Subject: [PATCH 05/13] Disable unblock button --- .../securesms/database/RecipientDatabase.java | 2 +- .../securesms/database/Storage.kt | 2 +- .../preferences/BlockedContactsActivity.kt | 78 ++++++------------- .../preferences/BlockedContactsAdapter.kt | 16 ++-- .../preferences/BlockedContactsViewModel.kt | 68 +++++++++++++--- app/src/main/res/color/button_destructive.xml | 5 ++ ...ctive_outline_button_medium_background.xml | 2 +- app/src/main/res/values/styles.xml | 2 +- .../libsession/database/StorageProtocol.kt | 2 +- 9 files changed, 98 insertions(+), 79 deletions(-) create mode 100644 app/src/main/res/color/button_destructive.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index af2faaaca9..e3570fd283 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -276,7 +276,7 @@ public class RecipientDatabase extends Database { notifyRecipientListeners(); } - public void setBlocked(@NonNull List recipients, boolean blocked) { + public void setBlocked(@NonNull Iterable recipients, boolean blocked) { SQLiteDatabase db = getWritableDatabase(); db.beginTransaction(); try { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 33896803b8..ed6510f741 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -1010,7 +1010,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseComponent.get(context).reactionDatabase().deleteMessageReactions(MessageId(messageId, mms)) } - override fun unblock(toUnblock: List) { + override fun unblock(toUnblock: Iterable) { val recipientDb = DatabaseComponent.get(context).recipientDatabase() recipientDb.setBlocked(toUnblock, false) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsActivity.kt index 504194d3a4..9b8d800dd5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsActivity.kt @@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.preferences import android.app.AlertDialog import android.os.Bundle -import android.view.View import androidx.activity.viewModels import androidx.core.view.isVisible import dagger.hilt.android.AndroidEntryPoint @@ -11,58 +10,31 @@ import network.loki.messenger.databinding.ActivityBlockedContactsBinding import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity @AndroidEntryPoint -class BlockedContactsActivity: PassphraseRequiredActionBarActivity(), View.OnClickListener { +class BlockedContactsActivity: PassphraseRequiredActionBarActivity() { lateinit var binding: ActivityBlockedContactsBinding val viewModel: BlockedContactsViewModel by viewModels() - val adapter = BlockedContactsAdapter() + val adapter: BlockedContactsAdapter by lazy { BlockedContactsAdapter(viewModel) } - override fun onClick(v: View?) { - if (v === binding.unblockButton && adapter.getSelectedItems().isNotEmpty()) { - val contactsToUnblock = adapter.getSelectedItems() - // show dialog - val title = if (contactsToUnblock.size == 1) { - getString(R.string.Unblock_dialog__title_single, contactsToUnblock.first().name) - } else { - getString(R.string.Unblock_dialog__title_multiple) + fun unblock() { + // show dialog + val title = viewModel.getTitle(this) + + val message = viewModel.getMessage(this) + + AlertDialog.Builder(this) + .setTitle(title) + .setMessage(message) + .setPositiveButton(R.string.continue_2) { d, _ -> + viewModel.unblock() + d.dismiss() } - - val message = if (contactsToUnblock.size == 1) { - getString(R.string.Unblock_dialog__message, contactsToUnblock.first().name) - } else { - val stringBuilder = StringBuilder() - val iterator = contactsToUnblock.iterator() - var numberAdded = 0 - while (iterator.hasNext() && numberAdded < 3) { - val nextRecipient = iterator.next() - if (numberAdded > 0) stringBuilder.append(", ") - - stringBuilder.append(nextRecipient.name) - numberAdded++ - } - val overflow = contactsToUnblock.size - numberAdded - if (overflow > 0) { - stringBuilder.append(" ") - val string = resources.getQuantityString(R.plurals.Unblock_dialog__message_multiple_overflow, overflow) - stringBuilder.append(string.format(overflow)) - } - getString(R.string.Unblock_dialog__message, stringBuilder.toString()) + .setNegativeButton(R.string.cancel) { d, _ -> + d.dismiss() } - - AlertDialog.Builder(this) - .setTitle(title) - .setMessage(message) - .setPositiveButton(R.string.continue_2) { d, _ -> - viewModel.unblock(contactsToUnblock) - d.dismiss() - } - .setNegativeButton(R.string.cancel) { d, _ -> - d.dismiss() - } - .show() - } + .show() } override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { @@ -73,15 +45,15 @@ class BlockedContactsActivity: PassphraseRequiredActionBarActivity(), View.OnCli binding.recyclerView.adapter = adapter viewModel.subscribe(this) - .observe(this) { newState -> - adapter.submitList(newState.blockedContacts) - val isEmpty = newState.blockedContacts.isEmpty() - binding.emptyStateMessageTextView.isVisible = isEmpty - binding.nonEmptyStateGroup.isVisible = !isEmpty + .observe(this) { state -> + adapter.submitList(state.blockedContacts) + binding.emptyStateMessageTextView.isVisible = state.emptyStateMessageTextViewVisible + binding.nonEmptyStateGroup.isVisible = state.nonEmptyStateGroupVisible + binding.unblockButton.isEnabled = state.unblockButtonEnabled } - binding.unblockButton.setOnClickListener(this) + binding.unblockButton.setOnClickListener { unblock() } } - -} \ No newline at end of file +} + \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt index 50af49b557..d400ab915c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt @@ -11,16 +11,14 @@ import network.loki.messenger.databinding.BlockedContactLayoutBinding import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.mms.GlideApp -class BlockedContactsAdapter: ListAdapter(RecipientDiffer()) { +class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdapter(RecipientDiffer()) { class RecipientDiffer: DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: Recipient, newItem: Recipient) = oldItem === newItem override fun areContentsTheSame(oldItem: Recipient, newItem: Recipient) = oldItem == newItem } - private val selectedItems = mutableListOf() - - fun getSelectedItems() = selectedItems + fun getSelectedItems() = viewModel.state.selectedItems override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val itemView = LayoutInflater.from(parent.context).inflate(R.layout.blocked_contact_layout, parent, false) @@ -28,19 +26,15 @@ class BlockedContactsAdapter: ListAdapter(capacity = Channel.CONFLATED) - private val _contacts = MutableLiveData(BlockedContactsViewState(emptyList())) + private val _state = MutableLiveData(BlockedContactsViewState(emptyList(), emptySet())) + + val state get() = _state.value!! fun subscribe(context: Context): LiveData { executor.launch(IO) { @@ -45,21 +48,66 @@ class BlockedContactsViewModel @Inject constructor(private val storage: Storage) } executor.launch(IO) { for (update in listUpdateChannel) { - val blockedContactState = BlockedContactsViewState(storage.blockedContacts().sortedBy { it.name }) + val blockedContactState = state.copy( + blockedContacts = storage.blockedContacts().sortedBy { it.name } + ) withContext(Main) { - _contacts.value = blockedContactState + _state.value = blockedContactState } } } - return _contacts + return _state } - fun unblock(toUnblock: List) { - storage.unblock(toUnblock) + fun unblock() { + storage.unblock(state.selectedItems) + _state.value = state.copy(selectedItems = emptySet()) + } + + fun select(selectedItem: Recipient, isSelected: Boolean) { + _state.value = state.run { + if (isSelected) copy(selectedItems = selectedItems + selectedItem) + else copy(selectedItems = selectedItems - selectedItem) + } + } + + fun getTitle(context: Context): String = + if (state.selectedItems.size == 1) { + context.getString(R.string.Unblock_dialog__title_single, state.selectedItems.first().name) + } else { + context.getString(R.string.Unblock_dialog__title_multiple) + } + + fun getMessage(context: Context): String { + if (state.selectedItems.size == 1) { + return context.getString(R.string.Unblock_dialog__message, state.selectedItems.first().name) + } + val stringBuilder = StringBuilder() + val iterator = state.selectedItems.iterator() + var numberAdded = 0 + while (iterator.hasNext() && numberAdded < 3) { + val nextRecipient = iterator.next() + if (numberAdded > 0) stringBuilder.append(", ") + + stringBuilder.append(nextRecipient.name) + numberAdded++ + } + val overflow = state.selectedItems.size - numberAdded + if (overflow > 0) { + stringBuilder.append(" ") + val string = context.resources.getQuantityString(R.plurals.Unblock_dialog__message_multiple_overflow, overflow) + stringBuilder.append(string.format(overflow)) + } + return context.getString(R.string.Unblock_dialog__message, stringBuilder.toString()) } data class BlockedContactsViewState( - val blockedContacts: List - ) - -} \ No newline at end of file + val blockedContacts: List, + val selectedItems: Set + ) { + val isEmpty get() = blockedContacts.isEmpty() + val unblockButtonEnabled get() = selectedItems.isNotEmpty() + val emptyStateMessageTextViewVisible get() = blockedContacts.isEmpty() + val nonEmptyStateGroupVisible get() = blockedContacts.isNotEmpty() + } +} diff --git a/app/src/main/res/color/button_destructive.xml b/app/src/main/res/color/button_destructive.xml new file mode 100644 index 0000000000..cefbfed23a --- /dev/null +++ b/app/src/main/res/color/button_destructive.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/destructive_outline_button_medium_background.xml b/app/src/main/res/drawable/destructive_outline_button_medium_background.xml index 7db4da2ec4..ac41ee6217 100644 --- a/app/src/main/res/drawable/destructive_outline_button_medium_background.xml +++ b/app/src/main/res/drawable/destructive_outline_button_medium_background.xml @@ -7,5 +7,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 9fa6059885..f4b9d6dbaa 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -113,7 +113,7 @@ diff --git a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt index 48d15a18ac..dc78aec1e2 100644 --- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -202,6 +202,6 @@ interface StorageProtocol { fun removeReaction(emoji: String, messageTimestamp: Long, author: String, notifyUnread: Boolean) fun updateReactionIfNeeded(message: Message, sender: String, openGroupSentTimestamp: Long) fun deleteReactions(messageId: Long, mms: Boolean) - fun unblock(toUnblock: List) + fun unblock(toUnblock: Iterable) fun blockedContacts(): List } From 8ef6ec2125ff93540021365049aa5932ede0c695 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 22 May 2023 14:57:36 +0930 Subject: [PATCH 06/13] . --- .../securesms/preferences/BlockedContactsAdapter.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt index d400ab915c..26632e5aa6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt @@ -18,8 +18,6 @@ class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdap override fun areContentsTheSame(oldItem: Recipient, newItem: Recipient) = oldItem == newItem } - fun getSelectedItems() = viewModel.state.selectedItems - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val itemView = LayoutInflater.from(parent.context).inflate(R.layout.blocked_contact_layout, parent, false) return ViewHolder(itemView) From 4f2ef7f2af5458e829325acc9141de021b3ff60a Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 22 May 2023 15:46:12 +0930 Subject: [PATCH 07/13] Add SelectableItem<> --- .../preferences/BlockedContactsActivity.kt | 11 ++--- .../preferences/BlockedContactsAdapter.kt | 45 +++++++++---------- .../preferences/BlockedContactsViewModel.kt | 10 +++++ .../securesms/util/adapter/SelectableItem.kt | 3 ++ 4 files changed, 36 insertions(+), 33 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/adapter/SelectableItem.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsActivity.kt index 9b8d800dd5..d2db4fca43 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsActivity.kt @@ -27,13 +27,8 @@ class BlockedContactsActivity: PassphraseRequiredActionBarActivity() { AlertDialog.Builder(this) .setTitle(title) .setMessage(message) - .setPositiveButton(R.string.continue_2) { d, _ -> - viewModel.unblock() - d.dismiss() - } - .setNegativeButton(R.string.cancel) { d, _ -> - d.dismiss() - } + .setPositiveButton(R.string.continue_2) { _, _ -> viewModel.unblock() } + .setNegativeButton(R.string.cancel) { _, _ -> } .show() } @@ -46,7 +41,7 @@ class BlockedContactsActivity: PassphraseRequiredActionBarActivity() { viewModel.subscribe(this) .observe(this) { state -> - adapter.submitList(state.blockedContacts) + adapter.submitList(state.items) binding.emptyStateMessageTextView.isVisible = state.emptyStateMessageTextViewVisible binding.nonEmptyStateGroup.isVisible = state.nonEmptyStateGroupVisible binding.unblockButton.isEnabled = state.unblockButtonEnabled diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt index 26632e5aa6..6c13133a4e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt @@ -10,30 +10,26 @@ import network.loki.messenger.R import network.loki.messenger.databinding.BlockedContactLayoutBinding import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.mms.GlideApp +import org.thoughtcrime.securesms.util.adapter.SelectableItem -class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdapter(RecipientDiffer()) { +typealias SelectableRecipient = SelectableItem - class RecipientDiffer: DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Recipient, newItem: Recipient) = oldItem === newItem - override fun areContentsTheSame(oldItem: Recipient, newItem: Recipient) = oldItem == newItem +class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdapter(RecipientDiffer()) { + + class RecipientDiffer: DiffUtil.ItemCallback() { + override fun areItemsTheSame(old: SelectableRecipient, new: SelectableRecipient) = old.item.address == new.item.address + override fun areContentsTheSame(old: SelectableRecipient, new: SelectableRecipient) = old.isSelected == new.isSelected + override fun getChangePayload(old: SelectableRecipient, new: SelectableRecipient) = new.isSelected } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val itemView = LayoutInflater.from(parent.context).inflate(R.layout.blocked_contact_layout, parent, false) - return ViewHolder(itemView) - } - - private fun toggleSelection(recipient: Recipient, isSelected: Boolean, position: Int) { - viewModel.select(recipient, isSelected) - notifyItemChanged(position) - } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = + LayoutInflater.from(parent.context) + .inflate(R.layout.blocked_contact_layout, parent, false) + .let(::ViewHolder) override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val recipient = getItem(position) - val isSelected = recipient in viewModel.state.selectedItems - holder.bind(recipient, isSelected) { - toggleSelection(recipient, !isSelected, position) - } + val selectable = getItem(position) + holder.bind(selectable, viewModel::toggle) } override fun onViewRecycled(holder: ViewHolder) { @@ -46,15 +42,14 @@ class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdap val glide = GlideApp.with(itemView) val binding = BlockedContactLayoutBinding.bind(itemView) - fun bind(recipient: Recipient, isSelected: Boolean, toggleSelection: () -> Unit) { - binding.recipientName.text = recipient.name + fun bind(selectable: SelectableRecipient, toggle: (SelectableRecipient) -> Unit) { + binding.recipientName.text = selectable.item.name with (binding.profilePictureView.root) { glide = this@ViewHolder.glide - update(recipient) + update(selectable.item) } - binding.root.setOnClickListener { toggleSelection() } - binding.selectButton.isSelected = isSelected + binding.root.setOnClickListener { toggle(selectable) } + binding.selectButton.isSelected = selectable.isSelected } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt index baafa16455..0b13201220 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt @@ -21,6 +21,7 @@ import network.loki.messenger.R import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.Storage +import org.thoughtcrime.securesms.util.adapter.SelectableItem import javax.inject.Inject @HiltViewModel @@ -101,10 +102,19 @@ class BlockedContactsViewModel @Inject constructor(private val storage: Storage) return context.getString(R.string.Unblock_dialog__message, stringBuilder.toString()) } + fun toggle(selectable: SelectableItem) { + _state.value = state.run { + if (selectable.isSelected) copy(selectedItems = selectedItems - selectable.item) + else copy(selectedItems = selectedItems + selectable.item) + } + } + data class BlockedContactsViewState( val blockedContacts: List, val selectedItems: Set ) { + val items = blockedContacts.map { SelectableItem(it, it in selectedItems) } + val isEmpty get() = blockedContacts.isEmpty() val unblockButtonEnabled get() = selectedItems.isNotEmpty() val emptyStateMessageTextViewVisible get() = blockedContacts.isEmpty() diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/adapter/SelectableItem.kt b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/SelectableItem.kt new file mode 100644 index 0000000000..88b41d11cd --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/SelectableItem.kt @@ -0,0 +1,3 @@ +package org.thoughtcrime.securesms.util.adapter + +data class SelectableItem(val item: T, val isSelected: Boolean) From 76466e57de067be98c229436b7c212021f02e640 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 22 May 2023 17:03:26 +0930 Subject: [PATCH 08/13] Fix ripple --- app/src/main/res/layout/blocked_contact_layout.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/blocked_contact_layout.xml b/app/src/main/res/layout/blocked_contact_layout.xml index 673779cfd0..40d7f40dd3 100644 --- a/app/src/main/res/layout/blocked_contact_layout.xml +++ b/app/src/main/res/layout/blocked_contact_layout.xml @@ -7,6 +7,7 @@ android:paddingHorizontal="@dimen/medium_spacing" android:paddingVertical="@dimen/small_spacing" android:gravity="center_vertical" + android:background="?selectableItemBackground" android:id="@+id/backgroundContainer"> Date: Mon, 22 May 2023 22:20:52 +0930 Subject: [PATCH 09/13] polish --- .../preferences/BlockedContactsAdapter.kt | 10 ++++++ .../preferences/BlockedContactsViewModel.kt | 2 +- ...ctive_outline_button_medium_background.xml | 22 ++++++------ ...inent_outline_button_medium_background.xml | 22 ++++++------ .../res/layout/activity_blocked_contacts.xml | 35 ++++++++++++------- 5 files changed, 57 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt index 6c13133a4e..1bd3b28610 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt @@ -32,6 +32,12 @@ class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdap holder.bind(selectable, viewModel::toggle) } + override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList) { + val selectable = getItem(position) + if (payloads.isEmpty()) holder.bind(selectable, viewModel::toggle) + else holder.select(selectable.isSelected) + } + override fun onViewRecycled(holder: ViewHolder) { super.onViewRecycled(holder) holder.binding.profilePictureView.root.recycle() @@ -51,5 +57,9 @@ class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdap binding.root.setOnClickListener { toggle(selectable) } binding.selectButton.isSelected = selectable.isSelected } + + fun select(isSelected: Boolean) { + binding.selectButton.isSelected = isSelected + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt index 0b13201220..0c93b2cc09 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt @@ -104,7 +104,7 @@ class BlockedContactsViewModel @Inject constructor(private val storage: Storage) fun toggle(selectable: SelectableItem) { _state.value = state.run { - if (selectable.isSelected) copy(selectedItems = selectedItems - selectable.item) + if (selectable.item in selectedItems) copy(selectedItems = selectedItems - selectable.item) else copy(selectedItems = selectedItems + selectable.item) } } diff --git a/app/src/main/res/drawable/destructive_outline_button_medium_background.xml b/app/src/main/res/drawable/destructive_outline_button_medium_background.xml index ac41ee6217..c6e01ef98e 100644 --- a/app/src/main/res/drawable/destructive_outline_button_medium_background.xml +++ b/app/src/main/res/drawable/destructive_outline_button_medium_background.xml @@ -1,11 +1,13 @@ - - - - - - - - \ No newline at end of file + + + + + + + + + diff --git a/app/src/main/res/drawable/prominent_outline_button_medium_background.xml b/app/src/main/res/drawable/prominent_outline_button_medium_background.xml index ee3bec8f7f..4bde2f855c 100644 --- a/app/src/main/res/drawable/prominent_outline_button_medium_background.xml +++ b/app/src/main/res/drawable/prominent_outline_button_medium_background.xml @@ -1,11 +1,13 @@ - - - - - - - - \ No newline at end of file + + + + + + + + + diff --git a/app/src/main/res/layout/activity_blocked_contacts.xml b/app/src/main/res/layout/activity_blocked_contacts.xml index 69d0043009..f02ad7cb31 100644 --- a/app/src/main/res/layout/activity_blocked_contacts.xml +++ b/app/src/main/res/layout/activity_blocked_contacts.xml @@ -4,28 +4,37 @@ android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto"> - - + android:layout_width="match_parent" + android:layout_height="0dp"> + + + + + @@ -38,7 +47,7 @@ android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toBottomOf="@+id/recyclerView" + app:layout_constraintTop_toBottomOf="@+id/cardView" android:id="@+id/unblockButton" app:layout_constraintBottom_toBottomOf="parent" android:layout_marginVertical="@dimen/large_spacing" @@ -49,6 +58,6 @@ android:id="@+id/nonEmptyStateGroup" android:layout_width="wrap_content" android:layout_height="wrap_content" - app:constraint_referenced_ids="unblockButton,recyclerView"/> + app:constraint_referenced_ids="unblockButton,cardView"/> \ No newline at end of file From a295dfb248ec1bee54d4dfb8ec7eb1289535efa3 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 23 May 2023 14:56:03 +0930 Subject: [PATCH 10/13] Cleanup --- .../securesms/preferences/BlockedContactsAdapter.kt | 8 +++----- .../securesms/preferences/BlockedContactsViewModel.kt | 7 +++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt index 1bd3b28610..a75d53c4f1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt @@ -28,14 +28,12 @@ class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdap .let(::ViewHolder) override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val selectable = getItem(position) - holder.bind(selectable, viewModel::toggle) + holder.bind(getItem(position), viewModel::toggle) } override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList) { - val selectable = getItem(position) - if (payloads.isEmpty()) holder.bind(selectable, viewModel::toggle) - else holder.select(selectable.isSelected) + if (payloads.isEmpty()) holder.bind(getItem(position), viewModel::toggle) + else holder.select(getItem(position).isSelected) } override fun onViewRecycled(holder: ViewHolder) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt index 0c93b2cc09..b5d7995506 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsViewModel.kt @@ -31,7 +31,7 @@ class BlockedContactsViewModel @Inject constructor(private val storage: Storage) private val listUpdateChannel = Channel(capacity = Channel.CONFLATED) - private val _state = MutableLiveData(BlockedContactsViewState(emptyList(), emptySet())) + private val _state = MutableLiveData(BlockedContactsViewState()) val state get() = _state.value!! @@ -110,12 +110,11 @@ class BlockedContactsViewModel @Inject constructor(private val storage: Storage) } data class BlockedContactsViewState( - val blockedContacts: List, - val selectedItems: Set + val blockedContacts: List = emptyList(), + val selectedItems: Set = emptySet() ) { val items = blockedContacts.map { SelectableItem(it, it in selectedItems) } - val isEmpty get() = blockedContacts.isEmpty() val unblockButtonEnabled get() = selectedItems.isNotEmpty() val emptyStateMessageTextViewVisible get() = blockedContacts.isEmpty() val nonEmptyStateGroupVisible get() = blockedContacts.isNotEmpty() From ff124b8edc504e945be341f044896674f4a42f7a Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 23 May 2023 18:41:04 +0930 Subject: [PATCH 11/13] Fix ID divider color --- .../components/LabeledSeparatorView.kt | 70 ------------------- app/src/main/res/drawable/view_separator.xml | 6 ++ app/src/main/res/layout/activity_settings.xml | 2 +- .../res/layout/fragment_enter_public_key.xml | 2 +- app/src/main/res/layout/view_separator.xml | 36 +++++++--- 5 files changed, 33 insertions(+), 83 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/LabeledSeparatorView.kt create mode 100644 app/src/main/res/drawable/view_separator.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/LabeledSeparatorView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/LabeledSeparatorView.kt deleted file mode 100644 index df36719db2..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/LabeledSeparatorView.kt +++ /dev/null @@ -1,70 +0,0 @@ -package org.thoughtcrime.securesms.components - -import android.content.Context -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.Path -import android.util.AttributeSet -import android.view.LayoutInflater -import android.widget.RelativeLayout -import network.loki.messenger.R -import network.loki.messenger.databinding.ViewSeparatorBinding -import org.thoughtcrime.securesms.util.toPx -import org.session.libsession.utilities.ThemeUtil - -class LabeledSeparatorView : RelativeLayout { - - private lateinit var binding: ViewSeparatorBinding - private val path = Path() - - private val paint: Paint by lazy { - val result = Paint() - result.style = Paint.Style.STROKE - result.color = ThemeUtil.getThemedColor(context, R.attr.dividerHorizontal) - result.strokeWidth = toPx(1, resources).toFloat() - result.isAntiAlias = true - result - } - - // region Lifecycle - constructor(context: Context) : super(context) { - setUpViewHierarchy() - } - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { - setUpViewHierarchy() - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { - setUpViewHierarchy() - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { - setUpViewHierarchy() - } - - private fun setUpViewHierarchy() { - binding = ViewSeparatorBinding.inflate(LayoutInflater.from(context)) - val layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) - addView(binding.root, layoutParams) - setWillNotDraw(false) - } - // endregion - - // region Updating - override fun onDraw(c: Canvas) { - super.onDraw(c) - val w = width.toFloat() - val h = height.toFloat() - val hMargin = toPx(16, resources).toFloat() - path.reset() - path.moveTo(0.0f, h / 2) - path.lineTo(binding.titleTextView.left - hMargin, h / 2) - path.addRoundRect(binding.titleTextView.left - hMargin, toPx(1, resources).toFloat(), binding.titleTextView.right + hMargin, h - toPx(1, resources).toFloat(), h / 2, h / 2, Path.Direction.CCW) - path.moveTo(binding.titleTextView.right + hMargin, h / 2) - path.lineTo(w, h / 2) - path.close() - c.drawPath(path, paint) - } - // endregion -} \ No newline at end of file diff --git a/app/src/main/res/drawable/view_separator.xml b/app/src/main/res/drawable/view_separator.xml new file mode 100644 index 0000000000..314bd3c994 --- /dev/null +++ b/app/src/main/res/drawable/view_separator.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 72c7e655fd..023096e4d4 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -61,7 +61,7 @@ - - - + android:layout_height="wrap_content"> - + + + android:layout_height="wrap_content"> - \ No newline at end of file + + + + \ No newline at end of file From 55dd62240acd40fb8ccfa730d990229d60068cfb Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 23 May 2023 18:49:28 +0930 Subject: [PATCH 12/13] Fix size & space --- app/src/main/res/drawable/view_separator.xml | 2 +- app/src/main/res/layout/view_separator.xml | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/drawable/view_separator.xml b/app/src/main/res/drawable/view_separator.xml index 314bd3c994..27dd4bc967 100644 --- a/app/src/main/res/drawable/view_separator.xml +++ b/app/src/main/res/drawable/view_separator.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/view_separator.xml b/app/src/main/res/layout/view_separator.xml index 68d71d5f66..aca7f26d23 100644 --- a/app/src/main/res/layout/view_separator.xml +++ b/app/src/main/res/layout/view_separator.xml @@ -8,11 +8,13 @@ android:layout_gravity="center" android:background="?colorDividerBackground" android:layout_width="match_parent" - android:layout_height="1px"/> + android:layout_height="1dp"/> Date: Tue, 30 May 2023 11:44:20 +0930 Subject: [PATCH 13/13] Fix scroll to bottom button visibility (#1219) --- .../conversation/v2/ConversationActivityV2.kt | 46 +++++++++++-------- .../session/libsession/utilities/ViewUtils.kt | 3 +- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index f9385d14bc..07751cab9a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -21,7 +21,6 @@ import android.widget.Toast import androidx.activity.viewModels import androidx.annotation.DimenRes import androidx.appcompat.app.AlertDialog -import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider @@ -210,6 +209,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe val searchViewModel: SearchViewModel by viewModels() var searchViewItem: MenuItem? = null + private var emojiPickerVisible = false + private val isScrolledToBottom: Boolean get() = binding?.conversationRecyclerView?.isScrolledToBottom ?: true @@ -441,17 +442,22 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe handleRecyclerViewScrolled() } }) + + binding!!.conversationRecyclerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + showScrollToBottomButtonIfApplicable() + } } // called from onCreate private fun setUpToolBar() { - setSupportActionBar(binding?.toolbar) + val binding = binding ?: return + setSupportActionBar(binding.toolbar) val actionBar = supportActionBar ?: return val recipient = viewModel.recipient ?: return actionBar.title = "" actionBar.setDisplayHomeAsUpEnabled(true) actionBar.setHomeButtonEnabled(true) - binding!!.toolbarContent.conversationTitleView.text = when { + binding.toolbarContent.conversationTitleView.text = when { recipient.isLocalNumber -> getString(R.string.note_to_self) else -> recipient.toShortString() } @@ -461,13 +467,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe R.dimen.small_profile_picture_size } val size = resources.getDimension(sizeID).roundToInt() - binding!!.toolbarContent.profilePictureView.root.layoutParams = LinearLayout.LayoutParams(size, size) - binding!!.toolbarContent.profilePictureView.root.glide = glide + binding.toolbarContent.profilePictureView.root.layoutParams = LinearLayout.LayoutParams(size, size) + binding.toolbarContent.profilePictureView.root.glide = glide MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(viewModel.threadId, this) - val profilePictureView = binding!!.toolbarContent.profilePictureView.root - viewModel.recipient?.let { recipient -> - profilePictureView.update(recipient) - } + val profilePictureView = binding.toolbarContent.profilePictureView.root + viewModel.recipient?.let(profilePictureView::update) } // called from onCreate @@ -904,15 +908,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe val binding = binding ?: return val wasTypingIndicatorVisibleBefore = binding.typingIndicatorViewContainer.isVisible binding.typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom - binding.typingIndicatorViewContainer.isVisible - showOrHidScrollToBottomButton() + showScrollToBottomButtonIfApplicable() val firstVisiblePosition = layoutManager?.findFirstVisibleItemPosition() ?: -1 unreadCount = min(unreadCount, firstVisiblePosition).coerceAtLeast(0) updateUnreadCountIndicator() } - private fun showOrHidScrollToBottomButton(show: Boolean = true) { - binding?.scrollToBottomButton?.isVisible = show && !isScrolledToBottom && adapter.itemCount > 0 + private fun showScrollToBottomButtonIfApplicable() { + binding?.scrollToBottomButton?.isVisible = !emojiPickerVisible && !isScrolledToBottom && adapter.itemCount > 0 } private fun updateUnreadCountIndicator() { @@ -1084,21 +1087,26 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe Log.e("Loki", "Failed to show emoji picker", e) return } + + val binding = binding ?: return + + emojiPickerVisible = true ViewUtil.hideKeyboard(this, visibleMessageView) - binding?.reactionsShade?.isVisible = true - showOrHidScrollToBottomButton(false) - binding?.conversationRecyclerView?.suppressLayout(true) + binding.reactionsShade.isVisible = true + binding.scrollToBottomButton.isVisible = false + binding.conversationRecyclerView.suppressLayout(true) reactionDelegate.setOnActionSelectedListener(ReactionsToolbarListener(message)) reactionDelegate.setOnHideListener(object: ConversationReactionOverlay.OnHideListener { override fun startHide() { - binding?.reactionsShade?.let { + emojiPickerVisible = false + binding.reactionsShade.let { ViewUtil.fadeOut(it, resources.getInteger(R.integer.reaction_scrubber_hide_duration), View.GONE) } - showOrHidScrollToBottomButton(true) + showScrollToBottomButtonIfApplicable() } override fun onHide() { - binding?.conversationRecyclerView?.suppressLayout(false) + binding.conversationRecyclerView.suppressLayout(false) WindowUtil.setLightStatusBarFromTheme(this@ConversationActivityV2); WindowUtil.setLightNavigationBarFromTheme(this@ConversationActivityV2); diff --git a/libsession/src/main/java/org/session/libsession/utilities/ViewUtils.kt b/libsession/src/main/java/org/session/libsession/utilities/ViewUtils.kt index ea4932d05c..dfe0c3b451 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/ViewUtils.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/ViewUtils.kt @@ -5,6 +5,7 @@ import android.util.TypedValue import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.recyclerview.widget.RecyclerView +import kotlin.math.max @ColorInt fun Context.getColorFromAttr( @@ -17,4 +18,4 @@ fun Context.getColorFromAttr( } val RecyclerView.isScrolledToBottom: Boolean - get() = computeVerticalScrollOffset() + computeVerticalScrollExtent() >= computeVerticalScrollRange() + get() = max(0, computeVerticalScrollOffset()) + computeVerticalScrollExtent() >= computeVerticalScrollRange()