From 8d8def5321add6e21feb069f1a119679af6f6158 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Thu, 27 Mar 2025 13:38:12 +1100 Subject: [PATCH 01/43] [SES-3551] - Move libsession-util to remote repo (#1042) --- .gitmodules | 3 - app/build.gradle | 1 - .../components/ProfilePictureView.kt | 2 +- .../securesms/configs/ConfigToDatabaseSync.kt | 6 +- .../securesms/configs/ConfigUploader.kt | 4 +- .../DisappearingMessagesViewModel.kt | 2 +- .../v2/mention/MentionViewModel.kt | 4 +- .../securesms/database/Storage.kt | 5 +- .../groups/BaseGroupMembersViewModel.kt | 17 +- .../securesms/groups/GroupManagerV2Impl.kt | 19 +- .../securesms/groups/GroupPoller.kt | 7 +- .../securesms/groups/GroupPollerManager.kt | 1 + .../groups/handler/AdminStateSync.kt | 2 +- .../handler/CleanupInvitationHandler.kt | 6 +- .../handler/RemoveGroupMemberHandler.kt | 15 +- .../securesms/notifications/PushReceiver.kt | 5 +- .../notifications/PushRegistrationHandler.kt | 14 +- .../sskenvironment/ProfileManager.kt | 1 + build.gradle | 7 + jitpack.yml | 2 + libsession-util/.gitignore | 2 - libsession-util/build.gradle | 47 - libsession-util/libsession-util | 1 - .../libsession_util/InstrumentedTests.kt | 818 ------------------ libsession-util/src/main/AndroidManifest.xml | 4 - libsession-util/src/main/cpp/CMakeLists.txt | 75 -- libsession-util/src/main/cpp/blinded_key.cpp | 34 - libsession-util/src/main/cpp/config_base.cpp | 150 ---- libsession-util/src/main/cpp/config_base.h | 34 - .../src/main/cpp/config_common.cpp | 39 - libsession-util/src/main/cpp/contacts.cpp | 82 -- libsession-util/src/main/cpp/contacts.h | 108 --- libsession-util/src/main/cpp/conversation.cpp | 373 -------- libsession-util/src/main/cpp/conversation.h | 156 ---- libsession-util/src/main/cpp/group_info.cpp | 208 ----- libsession-util/src/main/cpp/group_info.h | 12 - libsession-util/src/main/cpp/group_keys.cpp | 336 ------- libsession-util/src/main/cpp/group_keys.h | 12 - .../src/main/cpp/group_members.cpp | 240 ----- libsession-util/src/main/cpp/group_members.h | 18 - libsession-util/src/main/cpp/jni_utils.h | 54 -- libsession-util/src/main/cpp/logging.cpp | 47 - libsession-util/src/main/cpp/user_groups.cpp | 346 -------- libsession-util/src/main/cpp/user_groups.h | 194 ----- libsession-util/src/main/cpp/user_profile.cpp | 119 --- libsession-util/src/main/cpp/user_profile.h | 14 - libsession-util/src/main/cpp/util.cpp | 414 --------- libsession-util/src/main/cpp/util.h | 34 - .../loki/messenger/libsession_util/Config.kt | 563 ------------ .../libsession_util/util/BaseCommunity.kt | 11 - .../libsession_util/util/BlindKeyAPI.kt | 15 - .../messenger/libsession_util/util/Contact.kt | 16 - .../libsession_util/util/Conversation.kt | 31 - .../libsession_util/util/ExpiryMode.kt | 17 - .../libsession_util/util/GroupInfo.kt | 94 -- .../libsession_util/util/GroupMember.kt | 99 --- .../messenger/libsession_util/util/Logger.kt | 11 - .../messenger/libsession_util/util/Sodium.kt | 27 - .../messenger/libsession_util/util/Utils.kt | 67 -- .../libsession_util/ExampleUnitTest.kt | 14 - .../libsignal/crypto/MnemonicCodecTest.kt | 116 --- libsession/build.gradle | 3 +- .../libsession/database/StorageProtocol.kt | 2 +- .../messaging/jobs/InviteContactsJob.kt | 2 +- .../sending_receiving/MessageSender.kt | 2 +- .../pollers/LegacyClosedGroupPollerV2.kt | 2 +- .../sending_receiving/pollers/Poller.kt | 3 +- .../utilities/ConfigFactoryProtocol.kt | 2 +- .../libsession/utilities/ConfigUtils.kt | 21 + .../libsession/utilities}/GroupDisplayInfo.kt | 3 +- .../session/libsignal/utilities/Namespace.kt | 22 - settings.gradle | 1 - 72 files changed, 100 insertions(+), 5138 deletions(-) create mode 100644 jitpack.yml delete mode 100644 libsession-util/.gitignore delete mode 100644 libsession-util/build.gradle delete mode 160000 libsession-util/libsession-util delete mode 100644 libsession-util/src/androidTest/java/network/loki/messenger/libsession_util/InstrumentedTests.kt delete mode 100644 libsession-util/src/main/AndroidManifest.xml delete mode 100644 libsession-util/src/main/cpp/CMakeLists.txt delete mode 100644 libsession-util/src/main/cpp/blinded_key.cpp delete mode 100644 libsession-util/src/main/cpp/config_base.cpp delete mode 100644 libsession-util/src/main/cpp/config_base.h delete mode 100644 libsession-util/src/main/cpp/config_common.cpp delete mode 100644 libsession-util/src/main/cpp/contacts.cpp delete mode 100644 libsession-util/src/main/cpp/contacts.h delete mode 100644 libsession-util/src/main/cpp/conversation.cpp delete mode 100644 libsession-util/src/main/cpp/conversation.h delete mode 100644 libsession-util/src/main/cpp/group_info.cpp delete mode 100644 libsession-util/src/main/cpp/group_info.h delete mode 100644 libsession-util/src/main/cpp/group_keys.cpp delete mode 100644 libsession-util/src/main/cpp/group_keys.h delete mode 100644 libsession-util/src/main/cpp/group_members.cpp delete mode 100644 libsession-util/src/main/cpp/group_members.h delete mode 100644 libsession-util/src/main/cpp/jni_utils.h delete mode 100644 libsession-util/src/main/cpp/logging.cpp delete mode 100644 libsession-util/src/main/cpp/user_groups.cpp delete mode 100644 libsession-util/src/main/cpp/user_groups.h delete mode 100644 libsession-util/src/main/cpp/user_profile.cpp delete mode 100644 libsession-util/src/main/cpp/user_profile.h delete mode 100644 libsession-util/src/main/cpp/util.cpp delete mode 100644 libsession-util/src/main/cpp/util.h delete mode 100644 libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt delete mode 100644 libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BaseCommunity.kt delete mode 100644 libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BlindKeyAPI.kt delete mode 100644 libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Contact.kt delete mode 100644 libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Conversation.kt delete mode 100644 libsession-util/src/main/java/network/loki/messenger/libsession_util/util/ExpiryMode.kt delete mode 100644 libsession-util/src/main/java/network/loki/messenger/libsession_util/util/GroupInfo.kt delete mode 100644 libsession-util/src/main/java/network/loki/messenger/libsession_util/util/GroupMember.kt delete mode 100644 libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Logger.kt delete mode 100644 libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Sodium.kt delete mode 100644 libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Utils.kt delete mode 100644 libsession-util/src/test/java/network/loki/messenger/libsession_util/ExampleUnitTest.kt delete mode 100644 libsession-util/src/test/java/org/session/libsignal/crypto/MnemonicCodecTest.kt create mode 100644 libsession/src/main/java/org/session/libsession/utilities/ConfigUtils.kt rename {libsession-util/src/main/java/network/loki/messenger/libsession_util/util => libsession/src/main/java/org/session/libsession/utilities}/GroupDisplayInfo.kt (74%) delete mode 100644 libsignal/src/main/java/org/session/libsignal/utilities/Namespace.kt diff --git a/.gitmodules b/.gitmodules index 784585cbf5c..e69de29bb2d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "libsession-util/libsession-util"] - path = libsession-util/libsession-util - url = https://github.com/session-foundation/libsession-util.git diff --git a/app/build.gradle b/app/build.gradle index 0d592f2eeb1..f4d9c11a7a5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -336,7 +336,6 @@ dependencies { implementation 'net.zetetic:sqlcipher-android:4.6.1@aar' implementation project(":libsignal") implementation project(":libsession") - implementation project(":libsession-util") implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxJsonVersion" implementation "com.github.session-foundation.session-android-curve-25519:curve25519-java:$curve25519Version" implementation project(":liblazysodium") diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt index 0385d16cb57..5c31ea462ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt @@ -96,7 +96,7 @@ class ProfilePictureView @JvmOverloads constructor( .getGroupMemberAddresses(address.toGroupString(), true) } else { storage.getMembers(address.toString()) - .map { Address.fromSerialized(it.accountIdString()) } + .map { Address.fromSerialized(it.accountId()) } }.sorted().take(2) if (members.size <= 1) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigToDatabaseSync.kt b/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigToDatabaseSync.kt index b437e780645..6ed860da3b7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigToDatabaseSync.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigToDatabaseSync.kt @@ -175,7 +175,7 @@ class ConfigToDatabaseSync @Inject constructor( val deleteAttachmentsBefore: Long? ) { constructor(groupInfoConfig: ReadableGroupInfoConfig) : this( - id = groupInfoConfig.id(), + id = AccountId(groupInfoConfig.id()), name = groupInfoConfig.getName(), destroyed = groupInfoConfig.isDestroyed(), deleteBefore = groupInfoConfig.getDeleteBefore(), @@ -302,7 +302,7 @@ class ConfigToDatabaseSync @Inject constructor( val groupThreadsToKeep = hashMapOf() for (closedGroup in userGroups.closedGroupInfo) { - val recipient = Recipient.from(context, fromSerialized(closedGroup.groupAccountId.hexString), false) + val recipient = Recipient.from(context, fromSerialized(closedGroup.groupAccountId), false) storage.setRecipientApprovedMe(recipient, true) storage.setRecipientApproved(recipient, !closedGroup.invited) profileManager.setName(context, recipient, closedGroup.name) @@ -316,7 +316,7 @@ class ConfigToDatabaseSync @Inject constructor( ) } - groupThreadsToKeep[closedGroup.groupAccountId] = threadId + groupThreadsToKeep[AccountId(closedGroup.groupAccountId)] = threadId storage.setPinned(threadId, closedGroup.priority == PRIORITY_PINNED) diff --git a/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigUploader.kt b/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigUploader.kt index b3ac2434162..5543142e672 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigUploader.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/configs/ConfigUploader.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope +import network.loki.messenger.libsession_util.Namespace import network.loki.messenger.libsession_util.util.ConfigPush import org.session.libsession.database.StorageProtocol import org.session.libsession.database.userAuth @@ -41,7 +42,6 @@ import org.session.libsession.utilities.getGroup import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.Namespace import org.session.libsignal.utilities.Snode import org.session.libsignal.utilities.retryWithUniformInterval import org.thoughtcrime.securesms.util.NetworkConnectivity @@ -138,7 +138,7 @@ class ConfigUploader @Inject constructor( configFactory.withUserConfigs { configs -> configs.userGroups.allClosedGroupInfo() } .asSequence() .filter { !it.destroyed && !it.kicked } - .map { it.groupAccountId } + .map { AccountId(it.groupAccountId) } .asFlow() }, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt index d8de893217c..39b0e34be66 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/disappearingmessages/DisappearingMessagesViewModel.kt @@ -62,7 +62,7 @@ class DisappearingMessagesViewModel( val isAdmin = when { recipient.isGroupV2Recipient -> { // Handle the new closed group functionality - storage.getMembers(recipient.address.toString()).any { it.accountIdString() == textSecurePreferences.getLocalNumber() && it.admin } + storage.getMembers(recipient.address.toString()).any { it.accountId() == textSecurePreferences.getLocalNumber() && it.admin } } recipient.isLegacyGroupRecipient -> { val groupRecord = groupDb.getGroup(recipient.address.toGroupString()).orNull() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/mention/MentionViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/mention/MentionViewModel.kt index b9dbea66814..9fa73151e49 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/mention/MentionViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/mention/MentionViewModel.kt @@ -100,7 +100,7 @@ class MentionViewModel( .map { it.toString() } } recipient.isGroupV2Recipient -> { - storage.getMembers(recipient.address.toString()).map { it.accountIdString() } + storage.getMembers(recipient.address.toString()).map { it.accountId() } } recipient.isCommunityRecipient -> mmsDatabase.getRecentChatMemberIDs(threadID, 20) @@ -128,7 +128,7 @@ class MentionViewModel( configFactory.withGroupConfigs(AccountId(recipient.address.toString())) { it.groupMembers.allWithStatus() .filter { (member, status) -> member.isAdminOrBeingPromoted(status) } - .mapTo(hashSetOf()) { (member, _) -> member.accountId.toString() } + .mapTo(hashSetOf()) { (member, _) -> member.accountId() } } } else { emptySet() 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 0bf94267614..80ffee4856e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -9,7 +9,6 @@ import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_PINN import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISIBLE import network.loki.messenger.libsession_util.util.BaseCommunityInfo import network.loki.messenger.libsession_util.util.ExpiryMode -import network.loki.messenger.libsession_util.util.GroupDisplayInfo import network.loki.messenger.libsession_util.util.GroupInfo import network.loki.messenger.libsession_util.util.UserPic import org.session.libsession.avatars.AvatarHelper @@ -56,6 +55,7 @@ import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.SnodeClock import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.fromSerialized +import org.session.libsession.utilities.GroupDisplayInfo import org.session.libsession.utilities.GroupRecord import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.ProfileKeyUtil @@ -67,6 +67,7 @@ import org.session.libsession.utilities.recipients.MessageType import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient.DisappearingState import org.session.libsession.utilities.recipients.getType +import org.session.libsession.utilities.upsertContact import org.session.libsignal.crypto.ecc.DjbECPublicKey import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.messages.SignalServiceAttachmentPointer @@ -1041,7 +1042,7 @@ open class Storage @Inject constructor( return configFactory.withGroupConfigs(AccountId(groupAccountId)) { configs -> val info = configs.groupInfo GroupDisplayInfo( - id = info.id(), + id = AccountId(info.id()), name = info.getName(), profilePic = info.getProfilePic(), expiryTimer = info.getExpiryTimer(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/BaseGroupMembersViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/BaseGroupMembersViewModel.kt index 19497e2690b..d0dc989a3af 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/BaseGroupMembersViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/BaseGroupMembersViewModel.kt @@ -16,11 +16,11 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext import network.loki.messenger.R import network.loki.messenger.libsession_util.allWithStatus -import network.loki.messenger.libsession_util.util.GroupDisplayInfo import network.loki.messenger.libsession_util.util.GroupMember import org.session.libsession.database.StorageProtocol import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.ConfigUpdateNotification +import org.session.libsession.utilities.GroupDisplayInfo import org.session.libsession.utilities.UsernameUtils import org.session.libsignal.utilities.AccountId @@ -74,11 +74,12 @@ abstract class BaseGroupMembersViewModel ( myAccountId: AccountId, amIAdmin: Boolean, ): GroupMemberState { - val isMyself = member.accountId == myAccountId + val memberAccountId = AccountId(member.accountId()) + val isMyself = memberAccountId == myAccountId val name = if (isMyself) { context.getString(R.string.you) } else { - usernameUtils.getContactNameWithAccountID(member.accountId.hexString, groupId) + usernameUtils.getContactNameWithAccountID(memberAccountId.hexString, groupId) } val highlightStatus = status in EnumSet.of( @@ -87,15 +88,15 @@ abstract class BaseGroupMembersViewModel ( ) return GroupMemberState( - accountId = member.accountId, + accountId = memberAccountId, name = name, - canRemove = amIAdmin && member.accountId != myAccountId + canRemove = amIAdmin && memberAccountId != myAccountId && !member.isAdminOrBeingPromoted(status) && !member.isRemoved(status), - canPromote = amIAdmin && member.accountId != myAccountId + canPromote = amIAdmin && memberAccountId != myAccountId && !member.isAdminOrBeingPromoted(status) && !member.isRemoved(status), - canResendPromotion = amIAdmin && member.accountId != myAccountId + canResendPromotion = amIAdmin && memberAccountId != myAccountId && status == GroupMember.Status.PROMOTION_FAILED && !member.isRemoved(status), - canResendInvite = amIAdmin && member.accountId != myAccountId + canResendInvite = amIAdmin && memberAccountId != myAccountId && !member.isRemoved(status) && (status == GroupMember.Status.INVITE_SENT || status == GroupMember.Status.INVITE_FAILED), status = status.takeIf { !isMyself }, // Status is only meant for other members diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt index 0e777144765..33abd7004bf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import network.loki.messenger.R import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISIBLE +import network.loki.messenger.libsession_util.Namespace import network.loki.messenger.libsession_util.util.Conversation import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.GroupInfo @@ -54,7 +55,6 @@ import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateM import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.Namespace import org.thoughtcrime.securesms.configs.ConfigUploader import org.thoughtcrime.securesms.database.LokiAPIDatabase import org.thoughtcrime.securesms.database.LokiMessageDatabase @@ -120,7 +120,7 @@ class GroupManagerV2Impl @Inject constructor( } val adminKey = checkNotNull(group.adminKey) { "Admin key is null for new group creation." } - val groupId = group.groupAccountId + val groupId = AccountId(group.groupAccountId) val memberAsRecipients = members.map { Recipient.from(application, Address.fromSerialized(it.hexString), false) @@ -267,7 +267,7 @@ class GroupManagerV2Impl @Inject constructor( } configs.rekey() - newMembers.map { configs.groupKeys.getSubAccountToken(it) } + newMembers.map { configs.groupKeys.getSubAccountToken(it.hexString) } } // Call un-revocate API on new members, in case they have been removed before @@ -452,7 +452,7 @@ class GroupManagerV2Impl @Inject constructor( val allMembers = config.groupMembers.all() allMembers.count { it.admin } == 1 && allMembers.first { it.admin } - .accountIdString() == storage.getUserPublicKey() + .accountId() == storage.getUserPublicKey() } if (group != null && !group.kicked && !weAreTheOnlyAdmin) { @@ -680,9 +680,10 @@ class GroupManagerV2Impl @Inject constructor( withTimeout(20_000L) { // We must tell the poller to poll once, as we could have received this invitation // in the background where the poller isn't running - groupPollerManager.pollOnce(group.groupAccountId) + val groupId = AccountId(group.groupAccountId) + groupPollerManager.pollOnce(groupId) - groupPollerManager.watchGroupPollingState(group.groupAccountId) + groupPollerManager.watchGroupPollingState(groupId) .filter { it.hadAtLeastOneSuccessfulPoll } .first() } @@ -698,12 +699,12 @@ class GroupManagerV2Impl @Inject constructor( // this will fail the first couple of times :) MessageSender.sendNonDurably( responseMessage, - Destination.ClosedGroup(group.groupAccountId.hexString), + Destination.ClosedGroup(group.groupAccountId), isSyncMessage = false ) } else { // If we are invited as admin, we can just update the group info ourselves - configFactory.withMutableGroupConfigs(group.groupAccountId) { configs -> + configFactory.withMutableGroupConfigs(AccountId(group.groupAccountId)) { configs -> configs.groupKeys.loadAdminKey(adminKey) configs.groupMembers.get(key)?.let { member -> @@ -832,7 +833,7 @@ class GroupManagerV2Impl @Inject constructor( val shouldAutoApprove = storage.getRecipientApproved(Address.fromSerialized(inviter.hexString)) val closedGroupInfo = GroupInfo.ClosedGroupInfo( - groupAccountId = groupId, + groupAccountId = groupId.hexString, adminKey = authDataOrAdminSeed.takeIf { fromPromotion }?.let { GroupInfo.ClosedGroupInfo.adminKeyFromSeed(it) }, authData = authDataOrAdminSeed.takeIf { !fromPromotion }, priority = PRIORITY_VISIBLE, diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt index 84012a6d1b8..a1ce8c81211 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPoller.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope +import network.loki.messenger.libsession_util.Namespace import org.session.libsession.messaging.jobs.BatchMessageReceiveJob import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageReceiveParameters @@ -29,7 +30,6 @@ import org.session.libsignal.database.LokiAPIDatabaseProtocol import org.session.libsignal.exceptions.NonRetryableException import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.Namespace import org.session.libsignal.utilities.Snode import org.thoughtcrime.securesms.util.AppVisibilityManager import org.thoughtcrime.securesms.util.getRootCause @@ -429,7 +429,10 @@ class GroupPoller( rawResponse = body, snode = snode, publicKey = groupId.hexString, - decrypt = it.groupKeys::decrypt, + decrypt = { data -> + val (decrypted, sender) = it.groupKeys.decrypt(data) ?: return@parseRawMessagesResponse null + decrypted to AccountId(sender) + }, namespace = Namespace.GROUP_MESSAGES(), ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPollerManager.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPollerManager.kt index cd58bc11a26..10a6d716533 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPollerManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupPollerManager.kt @@ -78,6 +78,7 @@ class GroupPollerManager @Inject constructor( configFactory.withUserConfigs { it.userGroups.allClosedGroupInfo() } .mapNotNullTo(hashSetOf()) { group -> group.groupAccountId.takeIf { group.shouldPoll } + ?.let(::AccountId) } } .distinctUntilChanged() diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/handler/AdminStateSync.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/handler/AdminStateSync.kt index 77b14ce680d..2809e938964 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/handler/AdminStateSync.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/handler/AdminStateSync.kt @@ -45,7 +45,7 @@ class AdminStateSync @Inject constructor( .asSequence() .mapNotNull { if ((it as? GroupInfo.ClosedGroupInfo)?.hasAdminKey() == true) { - it.groupAccountId + AccountId(it.groupAccountId) } else { null } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/handler/CleanupInvitationHandler.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/handler/CleanupInvitationHandler.kt index b7f493bc3ed..96b6c8747be 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/handler/CleanupInvitationHandler.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/handler/CleanupInvitationHandler.kt @@ -8,6 +8,7 @@ import network.loki.messenger.libsession_util.util.GroupMember import org.session.libsession.messaging.groups.GroupScope import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.utilities.AccountId import javax.inject.Inject /** @@ -37,8 +38,9 @@ class CleanupInvitationHandler @Inject constructor( .asSequence() .filter { !it.kicked && !it.destroyed && it.hasAdminKey() } .forEach { group -> - groupScope.launch(group.groupAccountId, debugName = "CleanupInvitationHandler") { - configFactory.withMutableGroupConfigs(group.groupAccountId) { configs -> + val groupId = AccountId(group.groupAccountId) + groupScope.launch(groupId, debugName = "CleanupInvitationHandler") { + configFactory.withMutableGroupConfigs(groupId) { configs -> configs.groupMembers .allWithStatus() .filter { it.second == GroupMember.Status.INVITE_SENDING } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/handler/RemoveGroupMemberHandler.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/handler/RemoveGroupMemberHandler.kt index 24117ec952a..7f0774804ec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/handler/RemoveGroupMemberHandler.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/handler/RemoveGroupMemberHandler.kt @@ -6,12 +6,12 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch import network.loki.messenger.R +import network.loki.messenger.libsession_util.Namespace import network.loki.messenger.libsession_util.ReadableGroupKeysConfig import network.loki.messenger.libsession_util.allWithStatus import network.loki.messenger.libsession_util.util.GroupMember @@ -38,7 +38,6 @@ import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.Namespace import org.session.libsession.messaging.groups.GroupScope import javax.inject.Inject import javax.inject.Singleton @@ -118,7 +117,7 @@ class RemoveGroupMemberHandler @Inject constructor( SnodeAPI.buildAuthenticatedRevokeSubKeyBatchRequest( groupAdminAuth = groupAuth, subAccountTokens = pendingRemovals.map { (member, _) -> - configs.groupKeys.getSubAccountToken(member.accountId) + configs.groupKeys.getSubAccountToken(member.accountId()) } ) ) { "Fail to create a revoke request" } @@ -145,7 +144,7 @@ class RemoveGroupMemberHandler @Inject constructor( memberSessionIDs = pendingRemovals .asSequence() .filter { (member, status) -> member.shouldRemoveMessages(status) } - .map { (member, _) -> member.accountIdString() }, + .map { (member, _) -> member.accountId() }, ), auth = groupAuth, ) @@ -178,7 +177,7 @@ class RemoveGroupMemberHandler @Inject constructor( // now we can go ahead and update the configs configFactory.withMutableGroupConfigs(groupAccountId) { configs -> pendingRemovals.forEach { (member, _) -> - configs.groupMembers.erase(member.accountIdString()) + configs.groupMembers.erase(member.accountId()) } configs.rekey() } @@ -200,7 +199,7 @@ class RemoveGroupMemberHandler @Inject constructor( messageDataProvider.markUserMessagesAsDeleted( threadId = threadId, until = until, - sender = member.accountIdString(), + sender = member.accountId(), displayedMessage = context.getString(R.string.deleteMessageDeletedGlobally) ) } catch (e: Exception) { @@ -258,11 +257,11 @@ class RemoveGroupMemberHandler @Inject constructor( data = Base64.encodeBytes( Sodium.encryptForMultipleSimple( messages = Array(pendingRemovals.size) { - pendingRemovals[it].accountId.pubKeyBytes + AccountId(pendingRemovals[it].accountId()).pubKeyBytes .plus(keys.currentGeneration().toString().toByteArray()) }, recipients = Array(pendingRemovals.size) { - pendingRemovals[it].accountId.pubKeyBytes + AccountId(pendingRemovals[it].accountId()).pubKeyBytes }, ed25519SecretKey = adminKey, domain = Sodium.KICKED_DOMAIN diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt index e34d41e1b75..bd396b61f90 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt @@ -5,7 +5,6 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.pm.PackageManager -import android.os.Debug import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -17,6 +16,7 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.serialization.json.Json import network.loki.messenger.R +import network.loki.messenger.libsession_util.Namespace import org.session.libsession.messaging.jobs.BatchMessageReceiveJob import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageReceiveParameters @@ -34,7 +34,6 @@ import org.session.libsignal.protos.SignalServiceProtos.Envelope import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.Namespace import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.home.HomeActivity @@ -180,7 +179,7 @@ class PushReceiver @Inject constructor( Log.d(TAG, "Successfully decrypted group message from $sender") return Envelope.parseFrom(envelopBytes) .toBuilder() - .setSource(sender.hexString) + .setSource(sender) .build() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistrationHandler.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistrationHandler.kt index 6c68e9c26d5..be2be256679 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistrationHandler.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistrationHandler.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.scan import kotlinx.coroutines.launch +import network.loki.messenger.libsession_util.Namespace import org.session.libsession.database.userAuth import org.session.libsession.messaging.notifications.TokenFetcher import org.session.libsession.snode.OwnedSwarmAuth @@ -23,7 +24,6 @@ import org.session.libsession.snode.SwarmAuth import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.Namespace import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.dependencies.ConfigFactory @@ -73,7 +73,8 @@ constructor( getGroupSubscriptions( token = token ) + mapOf( - SubscriptionKey(userAuth.accountId, token) to Subscription(userAuth, listOf(Namespace.DEFAULT())) + SubscriptionKey(userAuth.accountId, token) to Subscription(userAuth, listOf( + Namespace.DEFAULT())) ) } .scan, Pair, Map>?>( @@ -147,11 +148,12 @@ constructor( for (group in groups) { val adminKey = group.adminKey + val groupId = AccountId(group.groupAccountId) if (adminKey != null && adminKey.isNotEmpty()) { put( - SubscriptionKey(group.groupAccountId, token), + SubscriptionKey(groupId, token), Subscription( - auth = OwnedSwarmAuth.ofClosedGroup(group.groupAccountId, adminKey), + auth = OwnedSwarmAuth.ofClosedGroup(groupId, adminKey), namespaces = namespaces ) ) @@ -160,7 +162,7 @@ constructor( val authData = group.authData if (authData != null && authData.isNotEmpty()) { - val subscription = configFactory.getGroupAuth(group.groupAccountId) + val subscription = configFactory.getGroupAuth(groupId) ?.let { Subscription( auth = it, @@ -169,7 +171,7 @@ constructor( } if (subscription != null) { - put(SubscriptionKey(group.groupAccountId, token), subscription) + put(SubscriptionKey(groupId, token), subscription) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt index 44a5f846dc7..c0fa6a36f58 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt @@ -11,6 +11,7 @@ import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.SSKEnvironment import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient +import org.session.libsession.utilities.upsertContact import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.IdPrefix import org.thoughtcrime.securesms.database.RecipientDatabase diff --git a/build.gradle b/build.gradle index 6156b244c6e..b84664c0cd2 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,13 @@ plugins { allprojects { repositories { + maven { + url uri("https://oxen.rocks/session-foundation/libsession-util-android/maven") + content { + includeGroup('org.sessionfoundation') + } + } + google() mavenCentral() maven { diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 00000000000..1e41e00b7d1 --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,2 @@ +jdk: + - openjdk17 \ No newline at end of file diff --git a/libsession-util/.gitignore b/libsession-util/.gitignore deleted file mode 100644 index 606666622e9..00000000000 --- a/libsession-util/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/build -/.cxx/ diff --git a/libsession-util/build.gradle b/libsession-util/build.gradle deleted file mode 100644 index b56461986bf..00000000000 --- a/libsession-util/build.gradle +++ /dev/null @@ -1,47 +0,0 @@ -plugins { - id 'com.android.library' - id 'org.jetbrains.kotlin.android' -} - -android { - namespace 'network.loki.messenger.libsession_util' - - defaultConfig { - compileSdk androidCompileSdkVersion - minSdkVersion androidMinimumSdkVersion - targetSdkVersion androidCompileSdkVersion - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - externalNativeBuild { - cmake { - } - } - } - - buildTypes { - release { - minifyEnabled false - } - } - externalNativeBuild { - cmake { - path "src/main/cpp/CMakeLists.txt" - version "3.22.1+" - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 - } - kotlinOptions { - jvmTarget = '11' - } -} - -dependencies { - testImplementation 'junit:junit:4.13.2' - implementation(project(":libsignal")) - implementation "com.google.protobuf:protobuf-java:$protobufVersion" - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' -} \ No newline at end of file diff --git a/libsession-util/libsession-util b/libsession-util/libsession-util deleted file mode 160000 index 3eb9eb79235..00000000000 --- a/libsession-util/libsession-util +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3eb9eb79235d8454aa6fc56278230a1fa26c9fb7 diff --git a/libsession-util/src/androidTest/java/network/loki/messenger/libsession_util/InstrumentedTests.kt b/libsession-util/src/androidTest/java/network/loki/messenger/libsession_util/InstrumentedTests.kt deleted file mode 100644 index 2e641237c13..00000000000 --- a/libsession-util/src/androidTest/java/network/loki/messenger/libsession_util/InstrumentedTests.kt +++ /dev/null @@ -1,818 +0,0 @@ -package network.loki.messenger.libsession_util - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import network.loki.messenger.libsession_util.util.BaseCommunityInfo -import network.loki.messenger.libsession_util.util.Contact -import network.loki.messenger.libsession_util.util.Conversation -import network.loki.messenger.libsession_util.util.ExpiryMode -import network.loki.messenger.libsession_util.util.GroupMember -import network.loki.messenger.libsession_util.util.KeyPair -import network.loki.messenger.libsession_util.util.Sodium -import network.loki.messenger.libsession_util.util.UserPic -import org.hamcrest.CoreMatchers.equalTo -import org.hamcrest.CoreMatchers.hasItem -import org.hamcrest.CoreMatchers.not -import org.hamcrest.CoreMatchers.notNullValue -import org.hamcrest.MatcherAssert.assertThat -import org.junit.Assert.assertArrayEquals -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Test -import org.junit.runner.RunWith -import org.session.libsignal.utilities.Hex -import org.session.libsignal.utilities.IdPrefix -import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.AccountId - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class InstrumentedTests { - - val seed = - Hex.fromStringCondensed("0123456789abcdef0123456789abcdef00000000000000000000000000000000") - val groupSeed = - Hex.fromStringCondensed("0123456789abcdef0123456789abcdef11111111111111111111111111111111") - - private val keyPair: KeyPair - get() { - return Sodium.ed25519KeyPair(seed) - } - - private val groupKeyPair: KeyPair - get() { - return Sodium.ed25519KeyPair(groupSeed) - } - - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("network.loki.messenger.libsession_util.test", appContext.packageName) - } - - @Test - fun jni_test_sodium_kp_ed_curve() { - val kp = keyPair - val curvePkBytes = Sodium.ed25519PkToCurve25519(kp.pubKey) - - val edPk = kp.pubKey - val curvePk = curvePkBytes - - assertArrayEquals(Hex.fromStringCondensed("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"), edPk) - assertArrayEquals(Hex.fromStringCondensed("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"), curvePk) - assertArrayEquals(kp.secretKey.take(32).toByteArray(), seed) - } - - @Test - fun testDirtyEmptyString() { - val contacts = Contacts.newInstance(keyPair.secretKey) - val definitelyRealId = "050000000000000000000000000000000000000000000000000000000000000000" - val contact = contacts.getOrConstruct(definitelyRealId) - contacts.set(contact) - assertTrue(contacts.dirty()) - contacts.set(contact.copy(name = "test")) - assertTrue(contacts.dirty()) - val push = contacts.push() - contacts.confirmPushed(push.seqNo, "abc123") - contacts.dump() - contacts.set(contact.copy(name = "test2")) - contacts.set(contact.copy(name = "test")) - assertTrue(contacts.dirty()) - } - - @Test - fun test_multi_encrypt() { - val user = keyPair - val xUserKey = Sodium.ed25519PkToCurve25519(user.pubKey) - val groupKey = groupKeyPair - val xGroupKey = Sodium.ed25519PkToCurve25519(groupKey.pubKey) - - val test = "test" - - val encoded = Sodium.encryptForMultipleSimple(arrayOf(test.encodeToByteArray()), arrayOf(xUserKey), groupKey.secretKey, "test")!! - val decoded = Sodium.decryptForMultipleSimple(encoded, user.secretKey, xGroupKey, "test") - assertEquals(test, decoded?.decodeToString()) - } - - @Test - fun test_multi_encrypt_from_user_groups() { - val user = keyPair - val xUserKey = Sodium.ed25519PkToCurve25519(user.pubKey) - val groups = UserGroupsConfig.newInstance(user.secretKey) - val group = groups.createGroup() - val groupSk = group.adminKey!! - val groupPub = group.groupAccountId.pubKeyBytes - val groupXPub = Sodium.ed25519PkToCurve25519(groupPub) - - val test = "test" - - val encoded = Sodium.encryptForMultipleSimple(arrayOf(test.encodeToByteArray()), arrayOf(xUserKey), groupSk, "test")!! - val decoded = Sodium.decryptForMultipleSimple(encoded, user.secretKey, groupXPub, "test") - assertEquals(test, decoded?.decodeToString()) - } - - @Test - fun jni_contacts() { - val contacts = Contacts.newInstance(keyPair.secretKey) - val definitelyRealId = "050000000000000000000000000000000000000000000000000000000000000000" - assertNull(contacts.get(definitelyRealId)) - - // Should be an uninitialized contact apart from ID - val c = contacts.getOrConstruct(definitelyRealId) - assertEquals(definitelyRealId, c.id) - assertTrue(c.name.isEmpty()) - assertTrue(c.nickname.isEmpty()) - assertFalse(c.approved) - assertFalse(c.approvedMe) - assertFalse(c.blocked) - assertEquals(UserPic.DEFAULT, c.profilePicture) - - assertFalse(contacts.needsPush()) - assertFalse(contacts.needsDump()) - assertEquals(0, contacts.push().seqNo) - - c.name = "Joe" - c.nickname = "Joey" - c.approved = true - c.approvedMe = true - - contacts.set(c) - - val cSaved = contacts.get(definitelyRealId)!! - assertEquals("Joe", cSaved.name) - assertEquals("Joey", cSaved.nickname) - assertTrue(cSaved.approved) - assertTrue(cSaved.approvedMe) - assertFalse(cSaved.blocked) - assertEquals(UserPic.DEFAULT, cSaved.profilePicture) - - val push1 = contacts.push() - - assertEquals(1, push1.seqNo) - contacts.confirmPushed(push1.seqNo, "fakehash1") - assertFalse(contacts.needsPush()) - assertTrue(contacts.needsDump()) - - val contacts2 = Contacts.newInstance(keyPair.secretKey, contacts.dump()) - assertFalse(contacts.needsDump()) - assertFalse(contacts2.needsPush()) - assertFalse(contacts2.needsDump()) - - val anotherId = "051111111111111111111111111111111111111111111111111111111111111111" - val c2 = contacts2.getOrConstruct(anotherId) - contacts2.set(c2) - val push2 = contacts2.push() - assertEquals(2, push2.seqNo) - contacts2.confirmPushed(push2.seqNo, "fakehash2") - assertFalse(contacts2.needsPush()) - - contacts.merge("fakehash2" to push2.config) - - - assertFalse(contacts.needsPush()) - assertEquals(push2.seqNo, contacts.push().seqNo) - - val contactList = contacts.all().toList() - assertEquals(definitelyRealId, contactList[0].id) - assertEquals(anotherId, contactList[1].id) - assertEquals("Joey", contactList[0].nickname) - assertEquals("", contactList[1].nickname) - - contacts.erase(definitelyRealId) - - val thirdId ="052222222222222222222222222222222222222222222222222222222222222222" - val third = Contact( - id = thirdId, - nickname = "Nickname 3", - approved = true, - blocked = true, - profilePicture = UserPic("http://example.com/huge.bmp", "qwertyuio01234567890123456789012".encodeToByteArray()), - expiryMode = ExpiryMode.NONE - ) - contacts2.set(third) - assertTrue(contacts.needsPush()) - assertTrue(contacts2.needsPush()) - val toPush = contacts.push() - val toPush2 = contacts2.push() - assertEquals(toPush.seqNo, toPush2.seqNo) - assertThat(toPush2.config, not(equals(toPush.config))) - - contacts.confirmPushed(toPush.seqNo, "fakehash3a") - contacts2.confirmPushed(toPush2.seqNo, "fakehash3b") - - contacts.merge("fakehash3b" to toPush2.config) - contacts2.merge("fakehash3a" to toPush.config) - - assertTrue(contacts.needsPush()) - assertTrue(contacts2.needsPush()) - - val mergePush = contacts.push() - val mergePush2 = contacts2.push() - - assertEquals(mergePush.seqNo, mergePush2.seqNo) - assertArrayEquals(mergePush.config, mergePush2.config) - - assertTrue(mergePush.obsoleteHashes.containsAll(listOf("fakehash3b", "fakehash3a"))) - assertTrue(mergePush2.obsoleteHashes.containsAll(listOf("fakehash3b", "fakehash3a"))) - - } - - @Test - fun jni_accessible() { - val userProfile = UserProfile.newInstance(keyPair.secretKey) - assertNotNull(userProfile) - userProfile.free() - } - - @Test - fun jni_user_profile_c_api() { - val edSk = keyPair.secretKey - val userProfile = UserProfile.newInstance(edSk) - - // these should be false as empty config - assertFalse(userProfile.needsPush()) - assertFalse(userProfile.needsDump()) - - // Since it's empty there shouldn't be a name - assertNull(userProfile.getName()) - - // Don't need to push yet so this is just for testing - val (_, seqNo) = userProfile.push() // disregarding encrypted - assertEquals("UserProfile", userProfile.encryptionDomain()) - assertEquals(0, seqNo) - - // This should also be unset: - assertEquals(UserPic.DEFAULT, userProfile.getPic()) - - // Now let's go set a profile name and picture: - // not sending keylen like c api so cutting off the NOTSECRET in key for testing purposes - userProfile.setName("Kallie") - val newUserPic = UserPic("http://example.org/omg-pic-123.bmp", "secret78901234567890123456789012".encodeToByteArray()) - userProfile.setPic(newUserPic) - userProfile.setNtsPriority(9) - - // Retrieve them just to make sure they set properly: - assertEquals("Kallie", userProfile.getName()) - val pic = userProfile.getPic() - assertEquals("http://example.org/omg-pic-123.bmp", pic.url) - assertEquals("secret78901234567890123456789012", pic.key.decodeToString()) - - // Since we've made changes, we should need to push new config to the swarm, *and* should need - // to dump the updated state: - assertTrue(userProfile.needsPush()) - assertTrue(userProfile.needsDump()) - val (newToPush, newSeqNo) = userProfile.push() - - val expHash0 = - Hex.fromStringCondensed("ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965") - - val expectedPush1Decrypted = ("d" + - "1:#"+ "i1e" + - "1:&"+ "d"+ - "1:+"+ "i9e"+ - "1:n"+ "6:Kallie"+ - "1:p"+ "34:http://example.org/omg-pic-123.bmp"+ - "1:q"+ "32:secret78901234567890123456789012"+ - "e"+ - "1:<"+ "l"+ - "l"+ "i0e"+ "32:").encodeToByteArray() + expHash0 + ("de"+ "e"+ - "e"+ - "1:="+ "d"+ - "1:+" +"0:"+ - "1:n" +"0:"+ - "1:p" +"0:"+ - "1:q" +"0:"+ - "e"+ - "e").encodeToByteArray() - - assertEquals(1, newSeqNo) - // We haven't dumped, so still need to dump: - assertTrue(userProfile.needsDump()) - // We did call push but we haven't confirmed it as stored yet, so this will still return true: - assertTrue(userProfile.needsPush()) - - val dump = userProfile.dump() - // (in a real client we'd now store this to disk) - assertFalse(userProfile.needsDump()) - val expectedDump = ("d" + - "1:!"+ "i2e" + - "1:$").encodeToByteArray() + expectedPush1Decrypted.size.toString().encodeToByteArray() + - ":".encodeToByteArray() + expectedPush1Decrypted + - "1:(0:1:)le".encodeToByteArray()+ - "e".encodeToByteArray() - - assertArrayEquals(expectedDump, dump) - - userProfile.confirmPushed(newSeqNo, "fakehash1") - - val newConf = UserProfile.newInstance(edSk) - - val accepted = newConf.merge("fakehash1" to newToPush) - assertThat(accepted, hasItem("fakehash1")) - assertThat(accepted.size, equalTo(1)) - - assertTrue(newConf.needsDump()) - assertFalse(newConf.needsPush()) - val _ignore = newConf.dump() - assertFalse(newConf.needsDump()) - - - userProfile.setName("Raz") - newConf.setName("Nibbler") - newConf.setPic(UserPic("http://new.example.com/pic", "qwertyuio01234567890123456789012".encodeToByteArray())) - - val conf = userProfile.push() - val conf2 = newConf.push() - - userProfile.confirmPushed(conf.seqNo, "fakehash2") - newConf.confirmPushed(conf2.seqNo, "fakehash3") - - userProfile.dump() - - assertFalse(conf.config.contentEquals(conf2.config)) - - newConf.merge("fakehash2" to conf.config) - userProfile.merge("fakehash3" to conf2.config) - - assertTrue(newConf.needsPush()) - assertTrue(userProfile.needsPush()) - - val newSeq1 = userProfile.push() - - assertEquals(3, newSeq1.seqNo) - - userProfile.confirmPushed(newSeq1.seqNo, "fakehash4") - - // assume newConf push gets rejected as it was last to write and clear previous config by hash on oxenss - newConf.merge("fakehash4" to newSeq1.config) - - val newSeqMerge = newConf.push() - - newConf.confirmPushed(newSeqMerge.seqNo, "fakehash5") - - assertEquals("Raz", newConf.getName()) - assertEquals(3, newSeqMerge.seqNo) - - // userProfile device polls and merges - userProfile.merge("fakehash5" to newSeqMerge.config) - - val userConfigMerge = userProfile.push() - - assertEquals(3, userConfigMerge.seqNo) - - assertEquals("Raz", newConf.getName()) - assertEquals("Raz", userProfile.getName()) - - userProfile.free() - newConf.free() - } - - @Test - fun merge_resolves_conflicts() { - val kp = keyPair - val a = UserProfile.newInstance(kp.secretKey) - val b = UserProfile.newInstance(kp.secretKey) - a.setName("A") - val (aPush, aSeq) = a.push() - a.confirmPushed(aSeq, "hashfroma") - b.setName("B") - // polls and sees invalid state, has to merge - b.merge("hashfroma" to aPush) - val (bPush, bSeq) = b.push() - b.confirmPushed(bSeq, "hashfromb") - assertEquals("B", b.getName()) - assertEquals(1, aSeq) - assertEquals(2, bSeq) - a.merge("hashfromb" to bPush) - assertEquals(2, a.push().seqNo) - } - - @Test - fun jni_setting_getting() { - val userProfile = UserProfile.newInstance(keyPair.secretKey) - val newName = "test" - println("Name being set via JNI call: $newName") - userProfile.setName(newName) - val nameFromNative = userProfile.getName() - assertEquals(newName, nameFromNative) - println("Name received by JNI call: $nameFromNative") - assertTrue(userProfile.dirty()) - userProfile.free() - } - - @Test - fun jni_remove_all_test() { - val convos = ConversationVolatileConfig.newInstance(keyPair.secretKey) - assertEquals(0 /* number removed */, convos.eraseAll { true /* 'erase' every item */ }) - - val definitelyRealId = "050000000000000000000000000000000000000000000000000000000000000000" - val definitelyRealConvo = Conversation.OneToOne(definitelyRealId, System.currentTimeMillis(), false) - convos.set(definitelyRealConvo) - - val anotherDefinitelyReadId = "051111111111111111111111111111111111111111111111111111111111111111" - val anotherDefinitelyRealConvo = Conversation.OneToOne(anotherDefinitelyReadId, System.currentTimeMillis(), false) - convos.set(anotherDefinitelyRealConvo) - - assertEquals(2, convos.sizeOneToOnes()) - - val numErased = convos.eraseAll { convo -> - convo is Conversation.OneToOne && convo.accountId == definitelyRealId - } - assertEquals(1, numErased) - assertEquals(1, convos.sizeOneToOnes()) - } - - @Test - fun test_open_group_urls() { - val (base1, room1, pk1) = BaseCommunityInfo.parseFullUrl( - "https://example.com/" + - "someroom?public_key=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - )!! - - val (base2, room2, pk2) = BaseCommunityInfo.parseFullUrl( - "HTTPS://EXAMPLE.COM/" + - "someroom?public_key=0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF" - )!! - - val (base3, room3, pk3) = BaseCommunityInfo.parseFullUrl( - "HTTPS://EXAMPLE.COM/r/" + - "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF" - )!! - - val (base4, room4, pk4) = BaseCommunityInfo.parseFullUrl( - "http://example.com/r/" + - "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF" - )!! - - val (base5, room5, pk5) = BaseCommunityInfo.parseFullUrl( - "HTTPS://EXAMPLE.com:443/r/" + - "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF" - )!! - - val (base6, room6, pk6) = BaseCommunityInfo.parseFullUrl( - "HTTP://EXAMPLE.com:80/r/" + - "someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF" - )!! - - val (base7, room7, pk7) = BaseCommunityInfo.parseFullUrl( - "http://example.com:80/r/" + - "someroom?public_key=ASNFZ4mrze8BI0VniavN7wEjRWeJq83vASNFZ4mrze8" - )!! - val (base8, room8, pk8) = BaseCommunityInfo.parseFullUrl( - "http://example.com:80/r/" + - "someroom?public_key=yrtwk3hjixg66yjdeiuauk6p7hy1gtm8tgih55abrpnsxnpm3zzo" - )!! - - assertEquals("https://example.com", base1) - assertEquals("http://example.com", base4) - assertEquals(base1, base2) - assertEquals(base1, base3) - assertNotEquals(base1, base4) - assertEquals(base1, base5) - assertEquals(base4, base6) - assertEquals(base4, base7) - assertEquals(base4, base8) - assertEquals("someroom", room1) - assertEquals("someroom", room2) - assertEquals("someroom", room3) - assertEquals("someroom", room4) - assertEquals("someroom", room5) - assertEquals("someroom", room6) - assertEquals("someroom", room7) - assertEquals("someroom", room8) - assertEquals(Hex.toStringCondensed(pk1), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") - assertEquals(Hex.toStringCondensed(pk2), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") - assertEquals(Hex.toStringCondensed(pk3), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") - assertEquals(Hex.toStringCondensed(pk4), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") - assertEquals(Hex.toStringCondensed(pk5), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") - assertEquals(Hex.toStringCondensed(pk6), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") - assertEquals(Hex.toStringCondensed(pk7), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") - assertEquals(Hex.toStringCondensed(pk8), "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") - - } - - @Test - fun test_conversations() { - val convos = ConversationVolatileConfig.newInstance(keyPair.secretKey) - val definitelyRealId = "055000000000000000000000000000000000000000000000000000000000000000" - assertNull(convos.getOneToOne(definitelyRealId)) - assertTrue(convos.empty()) - assertEquals(0, convos.size()) - - val c = convos.getOrConstructOneToOne(definitelyRealId) - - assertEquals(definitelyRealId, c.accountId) - assertEquals(0, c.lastRead) - - assertFalse(convos.needsPush()) - assertFalse(convos.needsDump()) - assertEquals(0, convos.push().seqNo) - - val nowMs = System.currentTimeMillis() - - c.lastRead = nowMs - - convos.set(c) - - assertNull(convos.getLegacyClosedGroup(definitelyRealId)) - assertNotNull(convos.getOneToOne(definitelyRealId)) - assertEquals(nowMs, convos.getOneToOne(definitelyRealId)?.lastRead) - - assertTrue(convos.needsPush()) - assertTrue(convos.needsDump()) - - val openGroupPubKey = Hex.fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") - - val og = convos.getOrConstructCommunity("http://Example.ORG:5678", "SudokuRoom", openGroupPubKey) - val ogCommunity = og.baseCommunityInfo - - assertEquals("http://example.org:5678", ogCommunity.baseUrl) // Note: lower-case - assertEquals("sudokuroom", ogCommunity.room) // Note: lower-case - assertEquals(64, ogCommunity.pubKeyHex.length) - assertEquals("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", ogCommunity.pubKeyHex) - - og.unread = true - - convos.set(og) - - val (_, seqNo) = convos.push() - - assertEquals(1, seqNo) - - convos.confirmPushed(seqNo, "fakehash1") - - assertTrue(convos.needsDump()) - assertFalse(convos.needsPush()) - - val convos2 = ConversationVolatileConfig.newInstance(keyPair.secretKey, convos.dump()) - assertFalse(convos.needsPush()) - assertFalse(convos.needsDump()) - assertEquals(1, convos.push().seqNo) - assertFalse(convos.needsDump()) - - val x1 = convos2.getOneToOne(definitelyRealId)!! - assertEquals(nowMs, x1.lastRead) - assertEquals(definitelyRealId, x1.accountId) - assertEquals(false, x1.unread) - - val x2 = convos2.getCommunity("http://EXAMPLE.org:5678", "sudokuRoom")!! - val x2Info = x2.baseCommunityInfo - assertEquals("http://example.org:5678", x2Info.baseUrl) - assertEquals("sudokuroom", x2Info.room) - assertEquals(x2Info.pubKeyHex, Hex.toStringCondensed(openGroupPubKey)) - assertTrue(x2.unread) - - val anotherId = "051111111111111111111111111111111111111111111111111111111111111111" - val c2 = convos.getOrConstructOneToOne(anotherId) - c2.unread = true - convos2.set(c2) - - val c3 = convos.getOrConstructLegacyGroup( - "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - ) - c3.lastRead = nowMs - 50 - convos2.set(c3) - - assertTrue(convos2.needsPush()) - - val (toPush2, seqNo2) = convos2.push() - assertEquals(2, seqNo2) - - convos2.confirmPushed(seqNo2, "fakehash2") - convos.merge("fakehash2" to toPush2) - - assertFalse(convos.needsPush()) - assertEquals(seqNo2, convos.push().seqNo) - - val seen = mutableListOf() - for ((ind, conv) in listOf(convos, convos2).withIndex()) { - Log.e("Test","Testing seen from convo #$ind") - seen.clear() - assertEquals(4, conv.size()) - assertEquals(2, conv.sizeOneToOnes()) - assertEquals(1, conv.sizeCommunities()) - assertEquals(1, conv.sizeLegacyClosedGroups()) - assertFalse(conv.empty()) - val allConvos = conv.all() - for (convo in allConvos) { - when (convo) { - is Conversation.OneToOne -> seen.add("1-to-1: ${convo.accountId}") - is Conversation.Community -> seen.add("og: ${convo.baseCommunityInfo.baseUrl}/r/${convo.baseCommunityInfo.room}") - is Conversation.LegacyGroup -> seen.add("cl: ${convo.groupId}") - is Conversation.ClosedGroup -> TODO() - null -> { /* ignore null cases */ } - } - } - - assertTrue(seen.contains("1-to-1: 051111111111111111111111111111111111111111111111111111111111111111")) - assertTrue(seen.contains("1-to-1: 055000000000000000000000000000000000000000000000000000000000000000")) - assertTrue(seen.contains("og: http://example.org:5678/r/sudokuroom")) - assertTrue(seen.contains("cl: 05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")) - assertTrue(seen.size == 4) // for some reason iterative checks aren't working in test cases - } - - assertFalse(convos.needsPush()) - convos.eraseOneToOne("052000000000000000000000000000000000000000000000000000000000000000") - assertFalse(convos.needsPush()) - convos.eraseOneToOne("055000000000000000000000000000000000000000000000000000000000000000") - assertTrue(convos.needsPush()) - - assertEquals(1, convos.allOneToOnes().size) - assertEquals("051111111111111111111111111111111111111111111111111111111111111111", - convos.allOneToOnes().map(Conversation.OneToOne::accountId).first() - ) - assertEquals(1, convos.allCommunities().size) - assertEquals("http://example.org:5678", - convos.allCommunities().map { it.baseCommunityInfo.baseUrl }.first() - ) - assertEquals(1, convos.allLegacyClosedGroups().size) - assertEquals("05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - convos.allLegacyClosedGroups().map(Conversation.LegacyGroup::groupId).first() - ) - } - - @Test - fun testGroupInfo() { - val (groupPublic, groupSecret) = groupKeyPair - val (userPublic, userSecret) = keyPair - val userCurve = Sodium.ed25519PkToCurve25519(userPublic) - val infoConf = GroupInfoConfig.newInstance(groupPublic, groupSecret) - infoConf.setName("New Group") - assertEquals("New Group", infoConf.getName()) - infoConf.setCreated(System.currentTimeMillis()) - assertThat(infoConf.getCreated(), notNullValue()) - val memberConf = GroupMembersConfig.newInstance(groupPublic, groupSecret) - memberConf.set( - GroupMember( - sessionId = "05"+Hex.toStringCondensed(userCurve), - name = "User", - admin = true - ) - ) - val keys = GroupKeysConfig.newInstance( - userSecretKey = userSecret, - groupPublicKey = groupPublic, - groupSecretKey = groupSecret, - info = infoConf, - members = memberConf - ) - assertThat(keys.pendingKey(), notNullValue()) - - } - - @Test - fun testGroupInfoOtherWay() { - val (userPublic, userSecret) = keyPair - val userCurve = Sodium.ed25519PkToCurve25519(userPublic) - val groupConfig = UserGroupsConfig.newInstance(userSecret) - val group = groupConfig.createGroup() - val groupSecret = checkNotNull(group.adminKey) { - "adminKey must exist for created group" - } - val groupPublic = Hex.fromStringCondensed(group.groupAccountId.publicKey) - groupConfig.set(group) - val setGroup = groupConfig.getClosedGroup(group.groupAccountId.hexString) - assertThat(setGroup, notNullValue()) - assertTrue(setGroup?.adminKey?.isNotEmpty() == true) - val infoConf = GroupInfoConfig.newInstance(groupPublic, group.adminKey) - infoConf.setName("New Group") - assertEquals("New Group", infoConf.getName()) - infoConf.setCreated(System.currentTimeMillis()) - assertThat(infoConf.getCreated(), notNullValue()) - val memberConf = GroupMembersConfig.newInstance(groupPublic, groupSecret) - memberConf.set( - GroupMember( - sessionId = "05"+Hex.toStringCondensed(userCurve), - name = "User", - admin = true - ) - ) - val keys = GroupKeysConfig.newInstance( - userSecretKey = userSecret, - groupPublicKey = groupPublic, - groupSecretKey = groupSecret, - info = infoConf, - members = memberConf - ) - assertThat(keys.pendingKey(), notNullValue()) - } - - @Test - fun testGroupMembership() { - val (userPublic, userSecret) = keyPair - val userSessionId = AccountId(IdPrefix.STANDARD, Sodium.ed25519PkToCurve25519(userPublic)) - val groupConfig = UserGroupsConfig.newInstance(userSecret) - val group = groupConfig.createGroup() - groupConfig.set(group) - val groupMembersConfig = GroupMembersConfig.newInstance( - group.groupAccountId.pubKeyBytes, - checkNotNull(group.adminKey) { - "signing key must exist for the group" - } - ) - val toAdd = GroupMember(userSessionId.hexString, "user", admin = true) - groupMembersConfig.set( - toAdd - ) - assertThat(groupMembersConfig.all().size, equalTo(1)) - assertThat(groupMembersConfig.all(), hasItem(toAdd)) - } - - @Test - fun testNewGroupExists() { - val (_, userSecret) = keyPair - val groupConfig = UserGroupsConfig.newInstance(userSecret) - val group = groupConfig.createGroup() - groupConfig.set(group) - val allClosedGroups = groupConfig.all() - assertThat(allClosedGroups.size, equalTo(1)) - assertTrue(groupConfig.needsPush()) - } - - @Test - fun testNewGroupInfo() { - val (_, userSecret) = keyPair - val groupConfig = UserGroupsConfig.newInstance(userSecret) - val group = groupConfig.createGroup() - groupConfig.set(group) - val groupInfo = GroupInfoConfig.newInstance(group.groupAccountId.pubKeyBytes, group.adminKey) - groupInfo.setName("Test Group") - groupInfo.setDescription("This is a test group") - assertThat(groupInfo.getName(), equalTo("Test Group")) - assertThat(groupInfo.getDescription(), equalTo("This is a test group")) - } - - @Test - fun testGroupKeyConfig() { - val (userPubKey, userSecret) = keyPair - val groupConfig = UserGroupsConfig.newInstance(userSecret) - val group = groupConfig.createGroup() - groupConfig.set(group) - val groupInfo = GroupInfoConfig.newInstance(group.groupAccountId.pubKeyBytes, group.adminKey) - groupInfo.setName("test") - val groupMembers = GroupMembersConfig.newInstance(group.groupAccountId.pubKeyBytes, group.adminKey) - groupMembers.set( - GroupMember( - sessionId = AccountId(IdPrefix.STANDARD, Sodium.ed25519PkToCurve25519(userPubKey)).hexString, - name = "admin", - admin = true - ) - ) - val membersDump = groupMembers.dump() - val infoDump = groupInfo.dump() - - val ourKeyConfig = GroupKeysConfig.newInstance( - userSecretKey = userSecret, - groupPublicKey = group.groupAccountId.pubKeyBytes, - groupSecretKey = group.adminKey, - info = groupInfo, - members = groupMembers - ) - - assertThat(ourKeyConfig.needsRekey(), equalTo(false)) - val pushed = ourKeyConfig.pendingConfig()!! - val messageTimestamp = System.currentTimeMillis() - ourKeyConfig.loadKey(pushed, "testabc", messageTimestamp, groupInfo, groupMembers) - assertThat(ourKeyConfig.needsDump(), equalTo(true)) - ourKeyConfig.dump() - assertThat(ourKeyConfig.needsRekey(), equalTo(false)) - val mergeInfo = GroupInfoConfig.newInstance(group.groupAccountId.pubKeyBytes, group.adminKey, infoDump) - val mergeMembers = GroupMembersConfig.newInstance(group.groupAccountId.pubKeyBytes, group.adminKey, membersDump) - val mergeConfig = GroupKeysConfig.newInstance(userSecret, group.groupAccountId.pubKeyBytes, group.adminKey, info = mergeInfo, members = mergeMembers) - mergeConfig.loadKey(pushed, "testabc", messageTimestamp, mergeInfo, mergeMembers) - assertThat(mergeConfig.needsRekey(), equalTo(false)) - assertThat(mergeConfig.keys().size, equalTo(1)) - assertThat(ourKeyConfig.keys().size, equalTo(1)) - assertThat(mergeConfig.keys().first(), equalTo(ourKeyConfig.keys().first())) - assertThat(ourKeyConfig.groupKeys().size, equalTo(1)) - assertThat(mergeConfig.groupKeys().size, equalTo(1)) - assertThat(ourKeyConfig.groupKeys().first(), equalTo(mergeConfig.groupKeys().first())) - } - - @Test - fun testConvoVolatileSetAndGet() { - val (userPubKey, userSecret) = keyPair - val groupConfig = UserGroupsConfig.newInstance(userSecret) - val group = groupConfig.createGroup() - groupConfig.set(group) - val volatiles = ConversationVolatileConfig.newInstance(userSecret) - val conversation = Conversation.ClosedGroup( - group.groupAccountId.hexString, - System.currentTimeMillis(), - false - ) - volatiles.set(conversation) - assertThat(volatiles.all().size, equalTo(1)) - assertThat(volatiles.allClosedGroups().size, equalTo(1)) - } - -} diff --git a/libsession-util/src/main/AndroidManifest.xml b/libsession-util/src/main/AndroidManifest.xml deleted file mode 100644 index a5918e68abc..00000000000 --- a/libsession-util/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/libsession-util/src/main/cpp/CMakeLists.txt b/libsession-util/src/main/cpp/CMakeLists.txt deleted file mode 100644 index 97fc9a18172..00000000000 --- a/libsession-util/src/main/cpp/CMakeLists.txt +++ /dev/null @@ -1,75 +0,0 @@ -# For more information about using CMake with Android Studio, read the -# documentation: https://d.android.com/studio/projects/add-native-code.html - -# Sets the minimum version of CMake required to build the native library. - -cmake_minimum_required(VERSION 3.18.1) - -# Declares and names the project. - -project("session_util") - -# Compiles in C++17 mode -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) - -set(CMAKE_BUILD_TYPE Release) - -# Creates and names a library, sets it as either STATIC -# or SHARED, and provides the relative paths to its source code. -# You can define multiple libraries, and CMake builds them for you. -# Gradle automatically packages shared libraries with your APK. - -set(STATIC_BUNDLE ON) -set(ENABLE_ONIONREQ OFF) -add_subdirectory(../../../libsession-util libsession) - -set(SOURCES - user_profile.cpp - user_groups.cpp - config_base.cpp - contacts.cpp - conversation.cpp - blinded_key.cpp - util.cpp - group_members.cpp - group_keys.cpp - group_info.cpp - config_common.cpp - logging.cpp -) - -add_library( # Sets the name of the library. - session_util - # Sets the library as a shared library. - SHARED - # Provides a relative path to your source file(s). - ${SOURCES}) - -# Searches for a specified prebuilt library and stores the path as a -# variable. Because CMake includes system libraries in the search path by -# default, you only need to specify the name of the public NDK library -# you want to add. CMake verifies that the library exists before -# completing its build. - -find_library( # Sets the name of the path variable. - log-lib - # Specifies the name of the NDK library that - # you want CMake to locate. - log) - -# Specifies libraries CMake should link to your target library. You -# can link multiple libraries, such as libraries you define in this -# build script, prebuilt third-party libraries, or system libraries. - -target_link_libraries( # Specifies the target library. - session_util - PUBLIC - libsession::util - libsession::config - libsession::crypto - libsodium::sodium-internal - # Links the target library to the log library - # included in the NDK. - ${log-lib}) diff --git a/libsession-util/src/main/cpp/blinded_key.cpp b/libsession-util/src/main/cpp/blinded_key.cpp deleted file mode 100644 index 1ed1cfd5544..00000000000 --- a/libsession-util/src/main/cpp/blinded_key.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include -#include - -#include "util.h" -#include "jni_utils.h" - -// -// Created by Thomas Ruffie on 29/7/2024. -// - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_util_BlindKeyAPI_blindVersionKeyPair(JNIEnv *env, - jobject thiz, - jbyteArray ed25519_secret_key) { - return jni_utils::run_catching_cxx_exception_or_throws(env, [=] { - const auto [pk, sk] = session::blind_version_key_pair(util::ustring_from_bytes(env, ed25519_secret_key)); - - jclass kp_class = env->FindClass("network/loki/messenger/libsession_util/util/KeyPair"); - jmethodID kp_constructor = env->GetMethodID(kp_class, "", "([B[B)V"); - return env->NewObject(kp_class, kp_constructor, util::bytes_from_ustring(env, {pk.data(), pk.size()}), util::bytes_from_ustring(env, {sk.data(), sk.size()})); - }); -} -extern "C" -JNIEXPORT jbyteArray JNICALL -Java_network_loki_messenger_libsession_1util_util_BlindKeyAPI_blindVersionSign(JNIEnv *env, - jobject thiz, - jbyteArray ed25519_secret_key, - jlong timestamp) { - return jni_utils::run_catching_cxx_exception_or_throws(env, [=] { - auto bytes = session::blind_version_sign(util::ustring_from_bytes(env, ed25519_secret_key), session::Platform::android, timestamp); - return util::bytes_from_ustring(env, bytes); - }); -} \ No newline at end of file diff --git a/libsession-util/src/main/cpp/config_base.cpp b/libsession-util/src/main/cpp/config_base.cpp deleted file mode 100644 index 8017d2bc7da..00000000000 --- a/libsession-util/src/main/cpp/config_base.cpp +++ /dev/null @@ -1,150 +0,0 @@ -#include "config_base.h" -#include "util.h" -#include "jni_utils.h" - -extern "C" { -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_ConfigBase_dirty(JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto* configBase = ptrToConfigBase(env, thiz); - return configBase->is_dirty(); -} - -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_ConfigBase_needsPush(JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto config = ptrToConfigBase(env, thiz); - return config->needs_push(); -} - -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_ConfigBase_needsDump(JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto config = ptrToConfigBase(env, thiz); - return config->needs_dump(); -} - -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_ConfigBase_push(JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto config = ptrToConfigBase(env, thiz); - auto push_tuple = config->push(); - auto to_push_str = std::get<1>(push_tuple); - auto to_delete = std::get<2>(push_tuple); - - jbyteArray returnByteArray = util::bytes_from_ustring(env, to_push_str); - jlong seqNo = std::get<0>(push_tuple); - jclass returnObjectClass = env->FindClass("network/loki/messenger/libsession_util/util/ConfigPush"); - jclass stackClass = env->FindClass("java/util/Stack"); - jmethodID methodId = env->GetMethodID(returnObjectClass, "", "([BJLjava/util/List;)V"); - jmethodID stack_init = env->GetMethodID(stackClass, "", "()V"); - jobject our_stack = env->NewObject(stackClass, stack_init); - jmethodID push_stack = env->GetMethodID(stackClass, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); - for (auto entry : to_delete) { - auto entry_jstring = env->NewStringUTF(entry.data()); - env->CallObjectMethod(our_stack, push_stack, entry_jstring); - } - jobject returnObject = env->NewObject(returnObjectClass, methodId, returnByteArray, seqNo, our_stack); - return returnObject; -} - -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_ConfigBase_free(JNIEnv *env, jobject thiz) { - auto config = ptrToConfigBase(env, thiz); - delete config; -} - -JNIEXPORT jbyteArray JNICALL -Java_network_loki_messenger_libsession_1util_ConfigBase_dump(JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto config = ptrToConfigBase(env, thiz); - auto dumped = config->dump(); - jbyteArray bytes = util::bytes_from_ustring(env, dumped); - return bytes; -} - -JNIEXPORT jstring JNICALL -Java_network_loki_messenger_libsession_1util_ConfigBase_encryptionDomain(JNIEnv *env, - jobject thiz) { - auto conf = ptrToConfigBase(env, thiz); - return env->NewStringUTF(conf->encryption_domain()); -} - -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_ConfigBase_confirmPushed(JNIEnv *env, jobject thiz, - jlong seq_no, - jstring new_hash_jstring) { - std::lock_guard lock{util::util_mutex_}; - auto conf = ptrToConfigBase(env, thiz); - auto new_hash = env->GetStringUTFChars(new_hash_jstring, nullptr); - conf->confirm_pushed(seq_no, new_hash); - env->ReleaseStringUTFChars(new_hash_jstring, new_hash); -} - -#pragma clang diagnostic push -#pragma ide diagnostic ignored "bugprone-reserved-identifier" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_ConfigBase_merge___3Lkotlin_Pair_2(JNIEnv *env, jobject thiz, - jobjectArray to_merge) { - return jni_utils::run_catching_cxx_exception_or_throws(env, [=] { - std::lock_guard lock{util::util_mutex_}; - auto conf = ptrToConfigBase(env, thiz); - size_t number = env->GetArrayLength(to_merge); - std::vector> configs = {}; - for (int i = 0; i < number; i++) { - auto jElement = (jobject) env->GetObjectArrayElement(to_merge, i); - auto pair = extractHashAndData(env, jElement); - configs.push_back(pair); - } - auto returned = conf->merge(configs); - auto string_stack = util::build_string_stack(env, returned); - return string_stack; - }); -} - -#pragma clang diagnostic pop -} -extern "C" -JNIEXPORT jint JNICALL -Java_network_loki_messenger_libsession_1util_ConfigBase_configNamespace(JNIEnv *env, jobject thiz) { - auto conf = ptrToConfigBase(env, thiz); - return (std::int16_t) conf->storage_namespace(); -} -extern "C" -JNIEXPORT jclass JNICALL -Java_network_loki_messenger_libsession_1util_ConfigBase_00024Companion_kindFor(JNIEnv *env, - jobject thiz, - jint config_namespace) { - auto user_class = env->FindClass("network/loki/messenger/libsession_util/UserProfile"); - auto contact_class = env->FindClass("network/loki/messenger/libsession_util/Contacts"); - auto convo_volatile_class = env->FindClass("network/loki/messenger/libsession_util/ConversationVolatileConfig"); - auto group_list_class = env->FindClass("network/loki/messenger/libsession_util/UserGroupsConfig"); - switch (config_namespace) { - case (int)session::config::Namespace::UserProfile: - return user_class; - case (int)session::config::Namespace::Contacts: - return contact_class; - case (int)session::config::Namespace::ConvoInfoVolatile: - return convo_volatile_class; - case (int)session::config::Namespace::UserGroups: - return group_list_class; - default: - return nullptr; - } -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_ConfigBase_currentHashes(JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto conf = ptrToConfigBase(env, thiz); - jclass stack = env->FindClass("java/util/Stack"); - jmethodID init = env->GetMethodID(stack, "", "()V"); - jobject our_stack = env->NewObject(stack, init); - jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); - auto vec = conf->current_hashes(); - for (std::string element: vec) { - env->CallObjectMethod(our_stack, push, env->NewStringUTF(element.data())); - } - return our_stack; -} \ No newline at end of file diff --git a/libsession-util/src/main/cpp/config_base.h b/libsession-util/src/main/cpp/config_base.h deleted file mode 100644 index 3f6e6ceba8d..00000000000 --- a/libsession-util/src/main/cpp/config_base.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef SESSION_ANDROID_CONFIG_BASE_H -#define SESSION_ANDROID_CONFIG_BASE_H - -#include "session/config/base.hpp" -#include "util.h" -#include -#include - -inline session::config::ConfigBase* ptrToConfigBase(JNIEnv *env, jobject obj) { - jclass baseClass = env->FindClass("network/loki/messenger/libsession_util/ConfigBase"); - jfieldID pointerField = env->GetFieldID(baseClass, "pointer", "J"); - return (session::config::ConfigBase*) env->GetLongField(obj, pointerField); -} - -inline std::pair extractHashAndData(JNIEnv *env, jobject kotlin_pair) { - jclass pair = env->FindClass("kotlin/Pair"); - jfieldID first = env->GetFieldID(pair, "first", "Ljava/lang/Object;"); - jfieldID second = env->GetFieldID(pair, "second", "Ljava/lang/Object;"); - jstring hash_as_jstring = static_cast(env->GetObjectField(kotlin_pair, first)); - jbyteArray data_as_jbytes = static_cast(env->GetObjectField(kotlin_pair, second)); - auto hash_as_string = env->GetStringUTFChars(hash_as_jstring, nullptr); - auto data_as_ustring = util::ustring_from_bytes(env, data_as_jbytes); - auto ret_pair = std::pair{hash_as_string, data_as_ustring}; - env->ReleaseStringUTFChars(hash_as_jstring, hash_as_string); - return ret_pair; -} - -inline session::config::ConfigSig* ptrToConfigSig(JNIEnv* env, jobject obj) { - jclass sigClass = env->FindClass("network/loki/messenger/libsession_util/ConfigSig"); - jfieldID pointerField = env->GetFieldID(sigClass, "pointer", "J"); - return (session::config::ConfigSig*) env->GetLongField(obj, pointerField); -} - -#endif \ No newline at end of file diff --git a/libsession-util/src/main/cpp/config_common.cpp b/libsession-util/src/main/cpp/config_common.cpp deleted file mode 100644 index 848c816bf7f..00000000000 --- a/libsession-util/src/main/cpp/config_common.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include -#include "util.h" -#include "jni_utils.h" - -#include -#include -#include -#include - -extern "C" -JNIEXPORT jlong JNICALL -Java_network_loki_messenger_libsession_1util_ConfigKt_createConfigObject( - JNIEnv *env, - jclass _clazz, - jstring java_config_name, - jbyteArray ed25519_secret_key, - jbyteArray initial_dump) { - return jni_utils::run_catching_cxx_exception_or_throws(env, [=] { - auto config_name = util::string_from_jstring(env, java_config_name); - auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key); - auto initial = initial_dump - ? std::optional(util::ustring_from_bytes(env, initial_dump)) - : std::nullopt; - - - std::lock_guard lock{util::util_mutex_}; - if (config_name == "Contacts") { - return reinterpret_cast(new session::config::Contacts(secret_key, initial)); - } else if (config_name == "UserProfile") { - return reinterpret_cast(new session::config::UserProfile(secret_key, initial)); - } else if (config_name == "UserGroups") { - return reinterpret_cast(new session::config::UserGroups(secret_key, initial)); - } else if (config_name == "ConvoInfoVolatile") { - return reinterpret_cast(new session::config::ConvoInfoVolatile(secret_key, initial)); - } else { - throw std::invalid_argument("Unknown config name: " + config_name); - } - }); -} \ No newline at end of file diff --git a/libsession-util/src/main/cpp/contacts.cpp b/libsession-util/src/main/cpp/contacts.cpp deleted file mode 100644 index ac756b7bed3..00000000000 --- a/libsession-util/src/main/cpp/contacts.cpp +++ /dev/null @@ -1,82 +0,0 @@ -#include "contacts.h" -#include "util.h" -#include "jni_utils.h" - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_Contacts_get(JNIEnv *env, jobject thiz, - jstring account_id) { - // If an exception is thrown, return nullptr - return jni_utils::run_catching_cxx_exception_or( - [=]() -> jobject { - std::lock_guard lock{util::util_mutex_}; - auto contacts = ptrToContacts(env, thiz); - auto account_id_chars = env->GetStringUTFChars(account_id, nullptr); - auto contact = contacts->get(account_id_chars); - env->ReleaseStringUTFChars(account_id, account_id_chars); - if (!contact) return nullptr; - jobject j_contact = serialize_contact(env, contact.value()); - return j_contact; - }, - [](const char *) -> jobject { return nullptr; } - ); -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_Contacts_getOrConstruct(JNIEnv *env, jobject thiz, - jstring account_id) { - return jni_utils::run_catching_cxx_exception_or_throws(env, [=] { - std::lock_guard lock{util::util_mutex_}; - auto contacts = ptrToContacts(env, thiz); - auto account_id_chars = env->GetStringUTFChars(account_id, nullptr); - auto contact = contacts->get_or_construct(account_id_chars); - env->ReleaseStringUTFChars(account_id, account_id_chars); - return serialize_contact(env, contact); - }); -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_Contacts_set(JNIEnv *env, jobject thiz, - jobject contact) { - jni_utils::run_catching_cxx_exception_or_throws(env, [=] { - std::lock_guard lock{util::util_mutex_}; - auto contacts = ptrToContacts(env, thiz); - auto contact_info = deserialize_contact(env, contact, contacts); - contacts->set(contact_info); - }); -} - -extern "C" -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_Contacts_erase(JNIEnv *env, jobject thiz, - jstring account_id) { - return jni_utils::run_catching_cxx_exception_or_throws(env, [=] { - std::lock_guard lock{util::util_mutex_}; - auto contacts = ptrToContacts(env, thiz); - auto account_id_chars = env->GetStringUTFChars(account_id, nullptr); - - bool result = contacts->erase(account_id_chars); - env->ReleaseStringUTFChars(account_id, account_id_chars); - return result; - }); -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_Contacts_all(JNIEnv *env, jobject thiz) { - return jni_utils::run_catching_cxx_exception_or_throws(env, [=] { - std::lock_guard lock{util::util_mutex_}; - auto contacts = ptrToContacts(env, thiz); - jclass stack = env->FindClass("java/util/Stack"); - jmethodID init = env->GetMethodID(stack, "", "()V"); - jobject our_stack = env->NewObject(stack, init); - jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); - for (const auto &contact: *contacts) { - auto contact_obj = serialize_contact(env, contact); - env->CallObjectMethod(our_stack, push, contact_obj); - } - return our_stack; - }); -} diff --git a/libsession-util/src/main/cpp/contacts.h b/libsession-util/src/main/cpp/contacts.h deleted file mode 100644 index f091e195eab..00000000000 --- a/libsession-util/src/main/cpp/contacts.h +++ /dev/null @@ -1,108 +0,0 @@ -#ifndef SESSION_ANDROID_CONTACTS_H -#define SESSION_ANDROID_CONTACTS_H - -#include -#include "session/config/contacts.hpp" -#include "util.h" - -inline session::config::Contacts *ptrToContacts(JNIEnv *env, jobject obj) { - jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts"); - jfieldID pointerField = env->GetFieldID(contactsClass, "pointer", "J"); - return (session::config::Contacts *) env->GetLongField(obj, pointerField); -} - -inline jobject serialize_contact(JNIEnv *env, session::config::contact_info info) { - jclass contactClass = env->FindClass("network/loki/messenger/libsession_util/util/Contact"); - jmethodID constructor = env->GetMethodID(contactClass, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZZLnetwork/loki/messenger/libsession_util/util/UserPic;JLnetwork/loki/messenger/libsession_util/util/ExpiryMode;)V"); - jstring id = env->NewStringUTF(info.session_id.data()); - jstring name = env->NewStringUTF(info.name.data()); - jstring nickname = env->NewStringUTF(info.nickname.data()); - jboolean approved, approvedMe, blocked; - approved = info.approved; - approvedMe = info.approved_me; - blocked = info.blocked; - auto created = info.created; - jobject profilePic = util::serialize_user_pic(env, info.profile_picture); - jobject returnObj = env->NewObject(contactClass, constructor, id, name, nickname, approved, - approvedMe, blocked, profilePic, (jlong)info.priority, - util::serialize_expiry(env, info.exp_mode, info.exp_timer)); - return returnObj; -} - -inline session::config::contact_info deserialize_contact(JNIEnv *env, jobject info, session::config::Contacts *conf) { - jclass contactClass = env->FindClass("network/loki/messenger/libsession_util/util/Contact"); - - jfieldID getId, getName, getNick, getApproved, getApprovedMe, getBlocked, getUserPic, getPriority, getExpiry, getHidden; - getId = env->GetFieldID(contactClass, "id", "Ljava/lang/String;"); - getName = env->GetFieldID(contactClass, "name", "Ljava/lang/String;"); - getNick = env->GetFieldID(contactClass, "nickname", "Ljava/lang/String;"); - getApproved = env->GetFieldID(contactClass, "approved", "Z"); - getApprovedMe = env->GetFieldID(contactClass, "approvedMe", "Z"); - getBlocked = env->GetFieldID(contactClass, "blocked", "Z"); - getUserPic = env->GetFieldID(contactClass, "profilePicture", - "Lnetwork/loki/messenger/libsession_util/util/UserPic;"); - getPriority = env->GetFieldID(contactClass, "priority", "J"); - getExpiry = env->GetFieldID(contactClass, "expiryMode", "Lnetwork/loki/messenger/libsession_util/util/ExpiryMode;"); - jstring name, nickname, account_id; - account_id = static_cast(env->GetObjectField(info, getId)); - name = static_cast(env->GetObjectField(info, getName)); - nickname = static_cast(env->GetObjectField(info, getNick)); - bool approved, approvedMe, blocked, hidden; - int priority = env->GetLongField(info, getPriority); - approved = env->GetBooleanField(info, getApproved); - approvedMe = env->GetBooleanField(info, getApprovedMe); - blocked = env->GetBooleanField(info, getBlocked); - jobject user_pic = env->GetObjectField(info, getUserPic); - jobject expiry_mode = env->GetObjectField(info, getExpiry); - - auto expiry_pair = util::deserialize_expiry(env, expiry_mode); - - std::string url; - session::ustring key; - - if (user_pic != nullptr) { - auto deserialized_pic = util::deserialize_user_pic(env, user_pic); - auto url_jstring = deserialized_pic.first; - auto url_bytes = env->GetStringUTFChars(url_jstring, nullptr); - url = std::string(url_bytes); - env->ReleaseStringUTFChars(url_jstring, url_bytes); - key = util::ustring_from_bytes(env, deserialized_pic.second); - } - - auto account_id_bytes = env->GetStringUTFChars(account_id, nullptr); - auto name_bytes = name ? env->GetStringUTFChars(name, nullptr) : nullptr; - auto nickname_bytes = nickname ? env->GetStringUTFChars(nickname, nullptr) : nullptr; - - auto contact_info = conf->get_or_construct(account_id_bytes); - if (name_bytes) { - contact_info.name = name_bytes; - } - if (nickname_bytes) { - contact_info.nickname = nickname_bytes; - } - contact_info.approved = approved; - contact_info.approved_me = approvedMe; - contact_info.blocked = blocked; - if (!url.empty() && !key.empty()) { - contact_info.profile_picture = session::config::profile_pic(url, key); - } else { - contact_info.profile_picture = session::config::profile_pic(); - } - - env->ReleaseStringUTFChars(account_id, account_id_bytes); - if (name_bytes) { - env->ReleaseStringUTFChars(name, name_bytes); - } - if (nickname_bytes) { - env->ReleaseStringUTFChars(nickname, nickname_bytes); - } - - contact_info.priority = priority; - contact_info.exp_mode = expiry_pair.first; - contact_info.exp_timer = std::chrono::seconds(expiry_pair.second); - - return contact_info; -} - - -#endif //SESSION_ANDROID_CONTACTS_H diff --git a/libsession-util/src/main/cpp/conversation.cpp b/libsession-util/src/main/cpp/conversation.cpp deleted file mode 100644 index 3fb322533b9..00000000000 --- a/libsession-util/src/main/cpp/conversation.cpp +++ /dev/null @@ -1,373 +0,0 @@ -#include -#include "conversation.h" - - -extern "C" -JNIEXPORT jint JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_sizeOneToOnes(JNIEnv *env, - jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto conversations = ptrToConvoInfo(env, thiz); - return conversations->size_1to1(); -} - -extern "C" -JNIEXPORT jint JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_eraseAll(JNIEnv *env, - jobject thiz, - jobject predicate) { - std::lock_guard lock{util::util_mutex_}; - auto conversations = ptrToConvoInfo(env, thiz); - - jclass predicate_class = env->FindClass("kotlin/jvm/functions/Function1"); - jmethodID predicate_call = env->GetMethodID(predicate_class, "invoke", "(Ljava/lang/Object;)Ljava/lang/Object;"); - - jclass bool_class = env->FindClass("java/lang/Boolean"); - jmethodID bool_get = env->GetMethodID(bool_class, "booleanValue", "()Z"); - - int removed = 0; - auto to_erase = std::vector(); - - for (auto it = conversations->begin(); it != conversations->end(); ++it) { - auto result = env->CallObjectMethod(predicate, predicate_call, serialize_any(env, *it)); - bool bool_result = env->CallBooleanMethod(result, bool_get); - if (bool_result) { - to_erase.push_back(*it); - } - } - - for (auto & entry : to_erase) { - if (conversations->erase(entry)) { - removed++; - } - } - - return removed; -} - -extern "C" -JNIEXPORT jint JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_size(JNIEnv *env, - jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto config = ptrToConvoInfo(env, thiz); - return (jint)config->size(); -} -extern "C" -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_empty(JNIEnv *env, - jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto config = ptrToConvoInfo(env, thiz); - return config->empty(); -} -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_set(JNIEnv *env, - jobject thiz, - jobject to_store) { - std::lock_guard lock{util::util_mutex_}; - - auto convos = ptrToConvoInfo(env, thiz); - - jclass one_to_one = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$OneToOne"); - jclass open_group = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$Community"); - jclass legacy_closed_group = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$LegacyGroup"); - jclass closed_group = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$ClosedGroup"); - - jclass to_store_class = env->GetObjectClass(to_store); - if (env->IsSameObject(to_store_class, one_to_one)) { - // store as 1to1 - convos->set(deserialize_one_to_one(env, to_store, convos)); - } else if (env->IsSameObject(to_store_class,open_group)) { - // store as open_group - convos->set(deserialize_community(env, to_store, convos)); - } else if (env->IsSameObject(to_store_class,legacy_closed_group)) { - // store as legacy_closed_group - convos->set(deserialize_legacy_closed_group(env, to_store, convos)); - } else if (env->IsSameObject(to_store_class, closed_group)) { - // store as new closed group - convos->set(deserialize_closed_group(env, to_store, convos)); - } -} -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getOneToOne(JNIEnv *env, - jobject thiz, - jstring pub_key_hex) { - std::lock_guard lock{util::util_mutex_}; - auto convos = ptrToConvoInfo(env, thiz); - auto param = env->GetStringUTFChars(pub_key_hex, nullptr); - auto internal = convos->get_1to1(param); - env->ReleaseStringUTFChars(pub_key_hex, param); - if (internal) { - return serialize_one_to_one(env, *internal); - } - return nullptr; -} -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getOrConstructOneToOne( - JNIEnv *env, jobject thiz, jstring pub_key_hex) { - std::lock_guard lock{util::util_mutex_}; - auto convos = ptrToConvoInfo(env, thiz); - auto param = env->GetStringUTFChars(pub_key_hex, nullptr); - auto internal = convos->get_or_construct_1to1(param); - env->ReleaseStringUTFChars(pub_key_hex, param); - return serialize_one_to_one(env, internal); -} -extern "C" -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_eraseOneToOne(JNIEnv *env, - jobject thiz, - jstring pub_key_hex) { - std::lock_guard lock{util::util_mutex_}; - auto convos = ptrToConvoInfo(env, thiz); - auto param = env->GetStringUTFChars(pub_key_hex, nullptr); - auto result = convos->erase_1to1(param); - env->ReleaseStringUTFChars(pub_key_hex, param); - return result; -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getCommunity__Ljava_lang_String_2Ljava_lang_String_2( - JNIEnv *env, jobject thiz, jstring base_url, jstring room) { - std::lock_guard lock{util::util_mutex_}; - auto convos = ptrToConvoInfo(env, thiz); - auto base_url_chars = env->GetStringUTFChars(base_url, nullptr); - auto room_chars = env->GetStringUTFChars(room, nullptr); - auto open = convos->get_community(base_url_chars, room_chars); - if (open) { - auto serialized = serialize_open_group(env, *open); - return serialized; - } - return nullptr; -} -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getOrConstructCommunity__Ljava_lang_String_2Ljava_lang_String_2_3B( - JNIEnv *env, jobject thiz, jstring base_url, jstring room, jbyteArray pub_key) { - std::lock_guard lock{util::util_mutex_}; - auto convos = ptrToConvoInfo(env, thiz); - auto base_url_chars = env->GetStringUTFChars(base_url, nullptr); - auto room_chars = env->GetStringUTFChars(room, nullptr); - auto pub_key_ustring = util::ustring_from_bytes(env, pub_key); - auto open = convos->get_or_construct_community(base_url_chars, room_chars, pub_key_ustring); - auto serialized = serialize_open_group(env, open); - return serialized; -} -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getOrConstructCommunity__Ljava_lang_String_2Ljava_lang_String_2Ljava_lang_String_2( - JNIEnv *env, jobject thiz, jstring base_url, jstring room, jstring pub_key_hex) { - std::lock_guard lock{util::util_mutex_}; - auto convos = ptrToConvoInfo(env, thiz); - auto base_url_chars = env->GetStringUTFChars(base_url, nullptr); - auto room_chars = env->GetStringUTFChars(room, nullptr); - auto hex_chars = env->GetStringUTFChars(pub_key_hex, nullptr); - auto open = convos->get_or_construct_community(base_url_chars, room_chars, hex_chars); - env->ReleaseStringUTFChars(base_url, base_url_chars); - env->ReleaseStringUTFChars(room, room_chars); - env->ReleaseStringUTFChars(pub_key_hex, hex_chars); - auto serialized = serialize_open_group(env, open); - return serialized; -} -extern "C" -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_eraseCommunity__Lnetwork_loki_messenger_libsession_1util_util_Conversation_Community_2(JNIEnv *env, - jobject thiz, - jobject open_group) { - std::lock_guard lock{util::util_mutex_}; - auto convos = ptrToConvoInfo(env, thiz); - auto deserialized = deserialize_community(env, open_group, convos); - return convos->erase(deserialized); -} -extern "C" -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_eraseCommunity__Ljava_lang_String_2Ljava_lang_String_2( - JNIEnv *env, jobject thiz, jstring base_url, jstring room) { - std::lock_guard lock{util::util_mutex_}; - auto convos = ptrToConvoInfo(env, thiz); - auto base_url_chars = env->GetStringUTFChars(base_url, nullptr); - auto room_chars = env->GetStringUTFChars(room, nullptr); - auto result = convos->erase_community(base_url_chars, room_chars); - env->ReleaseStringUTFChars(base_url, base_url_chars); - env->ReleaseStringUTFChars(room, room_chars); - return result; -} -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getLegacyClosedGroup( - JNIEnv *env, jobject thiz, jstring group_id) { - std::lock_guard lock{util::util_mutex_}; - auto convos = ptrToConvoInfo(env, thiz); - auto id_chars = env->GetStringUTFChars(group_id, nullptr); - auto lgc = convos->get_legacy_group(id_chars); - env->ReleaseStringUTFChars(group_id, id_chars); - if (lgc) { - auto serialized = serialize_legacy_group(env, *lgc); - return serialized; - } - return nullptr; -} -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getOrConstructLegacyGroup( - JNIEnv *env, jobject thiz, jstring group_id) { - std::lock_guard lock{util::util_mutex_}; - auto convos = ptrToConvoInfo(env, thiz); - auto id_chars = env->GetStringUTFChars(group_id, nullptr); - auto lgc = convos->get_or_construct_legacy_group(id_chars); - env->ReleaseStringUTFChars(group_id, id_chars); - return serialize_legacy_group(env, lgc); -} -extern "C" -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_eraseLegacyClosedGroup( - JNIEnv *env, jobject thiz, jstring group_id) { - std::lock_guard lock{util::util_mutex_}; - auto convos = ptrToConvoInfo(env, thiz); - auto id_chars = env->GetStringUTFChars(group_id, nullptr); - auto result = convos->erase_legacy_group(id_chars); - env->ReleaseStringUTFChars(group_id, id_chars); - return result; -} -extern "C" -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_erase(JNIEnv *env, - jobject thiz, - jobject conversation) { - std::lock_guard lock{util::util_mutex_}; - auto convos = ptrToConvoInfo(env, thiz); - auto deserialized = deserialize_any(env, conversation, convos); - if (!deserialized.has_value()) return false; - return convos->erase(*deserialized); -} -extern "C" -JNIEXPORT jint JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_sizeCommunities(JNIEnv *env, - jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto convos = ptrToConvoInfo(env, thiz); - return convos->size_communities(); -} -extern "C" -JNIEXPORT jint JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_sizeLegacyClosedGroups( - JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto convos = ptrToConvoInfo(env, thiz); - return convos->size_legacy_groups(); -} -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_all(JNIEnv *env, - jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto convos = ptrToConvoInfo(env, thiz); - jclass stack = env->FindClass("java/util/Stack"); - jmethodID init = env->GetMethodID(stack, "", "()V"); - jobject our_stack = env->NewObject(stack, init); - jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); - for (const auto& convo : *convos) { - auto contact_obj = serialize_any(env, convo); - env->CallObjectMethod(our_stack, push, contact_obj); - } - return our_stack; -} -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_allOneToOnes(JNIEnv *env, - jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto convos = ptrToConvoInfo(env, thiz); - jclass stack = env->FindClass("java/util/Stack"); - jmethodID init = env->GetMethodID(stack, "", "()V"); - jobject our_stack = env->NewObject(stack, init); - jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); - for (auto contact = convos->begin_1to1(); contact != convos->end(); ++contact) - env->CallObjectMethod(our_stack, push, serialize_one_to_one(env, *contact)); - return our_stack; -} -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_allCommunities(JNIEnv *env, - jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto convos = ptrToConvoInfo(env, thiz); - jclass stack = env->FindClass("java/util/Stack"); - jmethodID init = env->GetMethodID(stack, "", "()V"); - jobject our_stack = env->NewObject(stack, init); - jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); - for (auto contact = convos->begin_communities(); contact != convos->end(); ++contact) - env->CallObjectMethod(our_stack, push, serialize_open_group(env, *contact)); - return our_stack; -} -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_allLegacyClosedGroups( - JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto convos = ptrToConvoInfo(env, thiz); - jclass stack = env->FindClass("java/util/Stack"); - jmethodID init = env->GetMethodID(stack, "", "()V"); - jobject our_stack = env->NewObject(stack, init); - jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); - for (auto contact = convos->begin_legacy_groups(); contact != convos->end(); ++contact) - env->CallObjectMethod(our_stack, push, serialize_legacy_group(env, *contact)); - return our_stack; -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_allClosedGroups(JNIEnv *env, - jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto convos = ptrToConvoInfo(env, thiz); - jclass stack = env->FindClass("java/util/Stack"); - jmethodID init = env->GetMethodID(stack, "", "()V"); - jobject our_stack = env->NewObject(stack, init); - jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); - for (auto contact = convos->begin_groups(); contact != convos->end(); ++contact) - env->CallObjectMethod(our_stack, push, serialize_closed_group(env, *contact)); - return our_stack; -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getClosedGroup(JNIEnv *env, - jobject thiz, - jstring session_id) { - auto config = ptrToConvoInfo(env, thiz); - auto session_id_bytes = env->GetStringUTFChars(session_id, nullptr); - auto group = config->get_group(session_id_bytes); - env->ReleaseStringUTFChars(session_id, session_id_bytes); - if (group) { - auto serialized = serialize_closed_group(env, *group); - return serialized; - } - return nullptr; -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_getOrConstructClosedGroup( - JNIEnv *env, jobject thiz, jstring session_id) { - auto config = ptrToConvoInfo(env, thiz); - auto session_id_bytes = env->GetStringUTFChars(session_id, nullptr); - auto group = config->get_or_construct_group(session_id_bytes); - env->ReleaseStringUTFChars(session_id, session_id_bytes); - return serialize_closed_group(env, group); -} - -extern "C" -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_ConversationVolatileConfig_eraseClosedGroup( - JNIEnv *env, jobject thiz, jstring session_id) { - auto config = ptrToConvoInfo(env, thiz); - auto session_id_bytes = env->GetStringUTFChars(session_id, nullptr); - auto erased = config->erase_group(session_id_bytes); - env->ReleaseStringUTFChars(session_id, session_id_bytes); - return erased; -} diff --git a/libsession-util/src/main/cpp/conversation.h b/libsession-util/src/main/cpp/conversation.h deleted file mode 100644 index 1a8b51e2bca..00000000000 --- a/libsession-util/src/main/cpp/conversation.h +++ /dev/null @@ -1,156 +0,0 @@ -#ifndef SESSION_ANDROID_CONVERSATION_H -#define SESSION_ANDROID_CONVERSATION_H - -#include -#include -#include "util.h" -#include "session/config/convo_info_volatile.hpp" - -inline session::config::ConvoInfoVolatile *ptrToConvoInfo(JNIEnv *env, jobject obj) { - jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/ConversationVolatileConfig"); - jfieldID pointerField = env->GetFieldID(contactsClass, "pointer", "J"); - return (session::config::ConvoInfoVolatile *) env->GetLongField(obj, pointerField); -} - -inline jobject serialize_one_to_one(JNIEnv *env, session::config::convo::one_to_one one_to_one) { - jclass clazz = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$OneToOne"); - jmethodID constructor = env->GetMethodID(clazz, "", "(Ljava/lang/String;JZ)V"); - auto account_id = env->NewStringUTF(one_to_one.session_id.data()); - auto last_read = one_to_one.last_read; - auto unread = one_to_one.unread; - jobject serialized = env->NewObject(clazz, constructor, account_id, last_read, unread); - return serialized; -} - -inline jobject serialize_open_group(JNIEnv *env, session::config::convo::community community) { - jclass clazz = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$Community"); - auto base_community = util::serialize_base_community(env, community); - jmethodID constructor = env->GetMethodID(clazz, "", - "(Lnetwork/loki/messenger/libsession_util/util/BaseCommunityInfo;JZ)V"); - auto last_read = community.last_read; - auto unread = community.unread; - jobject serialized = env->NewObject(clazz, constructor, base_community, last_read, unread); - return serialized; -} - -inline jobject serialize_legacy_group(JNIEnv *env, session::config::convo::legacy_group group) { - jclass clazz = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$LegacyGroup"); - jmethodID constructor = env->GetMethodID(clazz, "", "(Ljava/lang/String;JZ)V"); - auto group_id = env->NewStringUTF(group.id.data()); - auto last_read = group.last_read; - auto unread = group.unread; - jobject serialized = env->NewObject(clazz, constructor, group_id, last_read, unread); - return serialized; -} - -inline jobject serialize_closed_group(JNIEnv* env, session::config::convo::group group) { - jclass clazz = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$ClosedGroup"); - jmethodID constructor = env->GetMethodID(clazz, "", "(Ljava/lang/String;JZ)V"); - auto session_id = env->NewStringUTF(group.id.data()); - auto last_read = group.last_read; - auto unread = group.unread; - return env->NewObject(clazz, constructor, session_id, last_read, unread); -} - -inline jobject serialize_any(JNIEnv *env, session::config::convo::any any) { - if (auto* dm = std::get_if(&any)) { - return serialize_one_to_one(env, *dm); - } else if (auto* og = std::get_if(&any)) { - return serialize_open_group(env, *og); - } else if (auto* lgc = std::get_if(&any)) { - return serialize_legacy_group(env, *lgc); - } else if (auto* gc = std::get_if(&any)) { - return serialize_closed_group(env, *gc); - } - return nullptr; -} - -inline session::config::convo::one_to_one deserialize_one_to_one(JNIEnv *env, jobject info, session::config::ConvoInfoVolatile *conf) { - auto clazz = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$OneToOne"); - auto id_getter = env->GetFieldID(clazz, "accountId", "Ljava/lang/String;"); - auto last_read_getter = env->GetFieldID(clazz, "lastRead", "J"); - auto unread_getter = env->GetFieldID(clazz, "unread", "Z"); - jstring id = static_cast(env->GetObjectField(info, id_getter)); - auto id_chars = env->GetStringUTFChars(id, nullptr); - std::string id_string = std::string{id_chars}; - auto deserialized = conf->get_or_construct_1to1(id_string); - deserialized.last_read = env->GetLongField(info, last_read_getter); - deserialized.unread = env->GetBooleanField(info, unread_getter); - env->ReleaseStringUTFChars(id, id_chars); - return deserialized; -} - -inline session::config::convo::community deserialize_community(JNIEnv *env, jobject info, session::config::ConvoInfoVolatile *conf) { - auto clazz = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$Community"); - auto base_community_getter = env->GetFieldID(clazz, "baseCommunityInfo", "Lnetwork/loki/messenger/libsession_util/util/BaseCommunityInfo;"); - auto last_read_getter = env->GetFieldID(clazz, "lastRead", "J"); - auto unread_getter = env->GetFieldID(clazz, "unread", "Z"); - - auto base_community_info = env->GetObjectField(info, base_community_getter); - - auto base_community_deserialized = util::deserialize_base_community(env, base_community_info); - auto deserialized = conf->get_or_construct_community( - base_community_deserialized.base_url(), - base_community_deserialized.room(), - base_community_deserialized.pubkey() - ); - - deserialized.last_read = env->GetLongField(info, last_read_getter); - deserialized.unread = env->GetBooleanField(info, unread_getter); - - return deserialized; -} - -inline session::config::convo::legacy_group deserialize_legacy_closed_group(JNIEnv *env, jobject info, session::config::ConvoInfoVolatile *conf) { - auto clazz = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$LegacyGroup"); - auto group_id_getter = env->GetFieldID(clazz, "groupId", "Ljava/lang/String;"); - auto last_read_getter = env->GetFieldID(clazz, "lastRead", "J"); - auto unread_getter = env->GetFieldID(clazz, "unread", "Z"); - auto group_id = static_cast(env->GetObjectField(info, group_id_getter)); - auto group_id_bytes = env->GetStringUTFChars(group_id, nullptr); - auto group_id_string = std::string{group_id_bytes}; - auto deserialized = conf->get_or_construct_legacy_group(group_id_string); - deserialized.last_read = env->GetLongField(info, last_read_getter); - deserialized.unread = env->GetBooleanField(info, unread_getter); - env->ReleaseStringUTFChars(group_id, group_id_bytes); - return deserialized; -} - -inline session::config::convo::group deserialize_closed_group(JNIEnv* env, jobject info, session::config::ConvoInfoVolatile* conf) { - auto clazz = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$ClosedGroup"); - auto id_getter = env->GetFieldID(clazz, "accountId", "Ljava/lang/String;"); - auto last_read_getter = env->GetFieldID(clazz, "lastRead", "J"); - auto unread_getter = env->GetFieldID(clazz, "unread", "Z"); - auto session_id = (jstring)env->GetObjectField(info, id_getter); - auto session_id_bytes = env->GetStringUTFChars(session_id, nullptr); - auto last_read = env->GetLongField(info, last_read_getter); - auto unread = env->GetBooleanField(info, unread_getter); - - auto group = conf->get_or_construct_group(session_id_bytes); - group.last_read = last_read; - group.unread = unread; - - env->ReleaseStringUTFChars(session_id, session_id_bytes); - return group; -} - -inline std::optional deserialize_any(JNIEnv *env, jobject convo, session::config::ConvoInfoVolatile *conf) { - auto oto_class = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$OneToOne"); - auto og_class = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$Community"); - auto lgc_class = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$LegacyGroup"); - auto gc_class = env->FindClass("network/loki/messenger/libsession_util/util/Conversation$ClosedGroup"); - - auto object_class = env->GetObjectClass(convo); - if (env->IsSameObject(object_class, oto_class)) { - return session::config::convo::any{deserialize_one_to_one(env, convo, conf)}; - } else if (env->IsSameObject(object_class, og_class)) { - return session::config::convo::any{deserialize_community(env, convo, conf)}; - } else if (env->IsSameObject(object_class, lgc_class)) { - return session::config::convo::any{deserialize_legacy_closed_group(env, convo, conf)}; - } else if (env->IsSameObject(object_class, gc_class)) { - return session::config::convo::any{deserialize_closed_group(env, convo, conf)}; - } - return std::nullopt; -} - -#endif //SESSION_ANDROID_CONVERSATION_H \ No newline at end of file diff --git a/libsession-util/src/main/cpp/group_info.cpp b/libsession-util/src/main/cpp/group_info.cpp deleted file mode 100644 index c80db46d2a2..00000000000 --- a/libsession-util/src/main/cpp/group_info.cpp +++ /dev/null @@ -1,208 +0,0 @@ -#include -#include "group_info.h" -#include "session/config/groups/info.hpp" - -extern "C" -JNIEXPORT jlong JNICALL -Java_network_loki_messenger_libsession_1util_GroupInfoConfig_00024Companion_newInstance(JNIEnv *env, - jobject thiz, - jbyteArray pub_key, - jbyteArray secret_key, - jbyteArray initial_dump) { - std::lock_guard guard{util::util_mutex_}; - std::optional secret_key_optional{std::nullopt}; - std::optional initial_dump_optional{std::nullopt}; - auto pub_key_bytes = util::ustring_from_bytes(env, pub_key); - if (secret_key && env->GetArrayLength(secret_key) > 0) { - auto secret_key_bytes = util::ustring_from_bytes(env, secret_key); - secret_key_optional = secret_key_bytes; - } - if (initial_dump && env->GetArrayLength(initial_dump) > 0) { - auto initial_dump_bytes = util::ustring_from_bytes(env, initial_dump); - initial_dump_optional = initial_dump_bytes; - } - - auto* group_info = new session::config::groups::Info(pub_key_bytes, secret_key_optional, initial_dump_optional); - return reinterpret_cast(group_info); -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_GroupInfoConfig_destroyGroup(JNIEnv *env, - jobject thiz) { - std::lock_guard guard{util::util_mutex_}; - auto group_info = ptrToInfo(env, thiz); - group_info->destroy_group(); -} - - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_GroupInfoConfig_getCreated(JNIEnv *env, jobject thiz) { - std::lock_guard guard{util::util_mutex_}; - auto group_info = ptrToInfo(env, thiz); - return util::jlongFromOptional(env, group_info->get_created()); -} -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_GroupInfoConfig_getDeleteAttachmentsBefore(JNIEnv *env, - jobject thiz) { - std::lock_guard guard{util::util_mutex_}; - auto group_info = ptrToInfo(env, thiz); - return util::jlongFromOptional(env, group_info->get_delete_attach_before()); -} -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_GroupInfoConfig_getDeleteBefore(JNIEnv *env, - jobject thiz) { - std::lock_guard guard{util::util_mutex_}; - auto group_info = ptrToInfo(env, thiz); - return util::jlongFromOptional(env, group_info->get_delete_before()); -} - -extern "C" -JNIEXPORT jlong JNICALL -Java_network_loki_messenger_libsession_1util_GroupInfoConfig_getExpiryTimer(JNIEnv *env, - jobject thiz) { - std::lock_guard guard{util::util_mutex_}; - auto group_info = ptrToInfo(env, thiz); - auto timer = group_info->get_expiry_timer(); - if (!timer) { - return 0; - } - long long in_seconds = timer->count(); - return in_seconds; -} - -extern "C" -JNIEXPORT jstring JNICALL -Java_network_loki_messenger_libsession_1util_GroupInfoConfig_getName(JNIEnv *env, jobject thiz) { - std::lock_guard guard{util::util_mutex_}; - auto group_info = ptrToInfo(env, thiz); - return util::jstringFromOptional(env, group_info->get_name()); -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_GroupInfoConfig_getProfilePic(JNIEnv *env, - jobject thiz) { - std::lock_guard guard{util::util_mutex_}; - auto group_info = ptrToInfo(env, thiz); - return util::serialize_user_pic(env, group_info->get_profile_pic()); -} - -extern "C" -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_GroupInfoConfig_isDestroyed(JNIEnv *env, - jobject thiz) { - std::lock_guard guard{util::util_mutex_}; - auto group_info = ptrToInfo(env, thiz); - return group_info->is_destroyed(); -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_GroupInfoConfig_setCreated(JNIEnv *env, jobject thiz, - jlong created_at) { - std::lock_guard guard{util::util_mutex_}; - auto group_info = ptrToInfo(env, thiz); - group_info->set_created(created_at); -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_GroupInfoConfig_setDeleteAttachmentsBefore(JNIEnv *env, - jobject thiz, - jlong delete_before) { - std::lock_guard guard{util::util_mutex_}; - auto group_info = ptrToInfo(env, thiz); - group_info->set_delete_attach_before(delete_before); -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_GroupInfoConfig_setDeleteBefore(JNIEnv *env, - jobject thiz, - jlong delete_before) { - std::lock_guard guard{util::util_mutex_}; - auto group_info = ptrToInfo(env, thiz); - group_info->set_delete_before(delete_before); -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_GroupInfoConfig_setExpiryTimer(JNIEnv *env, - jobject thiz, - jlong expire_seconds) { - std::lock_guard guard{util::util_mutex_}; - auto group_info = ptrToInfo(env, thiz); - group_info->set_expiry_timer(std::chrono::seconds{expire_seconds}); -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_GroupInfoConfig_setName(JNIEnv *env, jobject thiz, - jstring new_name) { - std::lock_guard guard{util::util_mutex_}; - auto group_info = ptrToInfo(env, thiz); - auto bytes = env->GetStringUTFChars(new_name, nullptr); - group_info->set_name(bytes); - env->ReleaseStringUTFChars(new_name, bytes); -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_GroupInfoConfig_setProfilePic(JNIEnv *env, - jobject thiz, - jobject new_profile_pic) { - std::lock_guard guard{util::util_mutex_}; - auto group_info = ptrToInfo(env, thiz); - auto user_pic = util::deserialize_user_pic(env, new_profile_pic); - auto url = env->GetStringUTFChars(user_pic.first, nullptr); - auto key = util::ustring_from_bytes(env, user_pic.second); - group_info->set_profile_pic(url, key); - env->ReleaseStringUTFChars(user_pic.first, url); -} - -extern "C" -JNIEXPORT jlong JNICALL -Java_network_loki_messenger_libsession_1util_GroupInfoConfig_storageNamespace(JNIEnv *env, - jobject thiz) { - std::lock_guard guard{util::util_mutex_}; - auto group_info = ptrToInfo(env, thiz); - return static_cast(group_info->storage_namespace()); -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_GroupInfoConfig_id(JNIEnv *env, jobject thiz) { - std::lock_guard guard{util::util_mutex_}; - auto group_info = ptrToInfo(env, thiz); - return util::serialize_account_id(env, group_info->id); -} - -extern "C" -JNIEXPORT jstring JNICALL -Java_network_loki_messenger_libsession_1util_GroupInfoConfig_getDescription(JNIEnv *env, - jobject thiz) { - std::lock_guard guard{util::util_mutex_}; - auto group_info = ptrToInfo(env, thiz); - auto description = group_info->get_description(); - if (!description) { - return nullptr; - } - auto jstring = env->NewStringUTF(description->data()); - return jstring; -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_GroupInfoConfig_setDescription(JNIEnv *env, - jobject thiz, - jstring new_description) { - std::lock_guard guard{util::util_mutex_}; - auto group_info = ptrToInfo(env, thiz); - auto description = env->GetStringUTFChars(new_description, nullptr); - group_info->set_description(description); - env->ReleaseStringUTFChars(new_description, description); -} \ No newline at end of file diff --git a/libsession-util/src/main/cpp/group_info.h b/libsession-util/src/main/cpp/group_info.h deleted file mode 100644 index 9fadf53ca41..00000000000 --- a/libsession-util/src/main/cpp/group_info.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef SESSION_ANDROID_GROUP_INFO_H -#define SESSION_ANDROID_GROUP_INFO_H - -#include "util.h" - -inline session::config::groups::Info* ptrToInfo(JNIEnv* env, jobject obj) { - jclass configClass = env->FindClass("network/loki/messenger/libsession_util/GroupInfoConfig"); - jfieldID pointerField = env->GetFieldID(configClass, "pointer", "J"); - return (session::config::groups::Info*) env->GetLongField(obj, pointerField); -} - -#endif //SESSION_ANDROID_GROUP_INFO_H diff --git a/libsession-util/src/main/cpp/group_keys.cpp b/libsession-util/src/main/cpp/group_keys.cpp deleted file mode 100644 index d70d1aa36ef..00000000000 --- a/libsession-util/src/main/cpp/group_keys.cpp +++ /dev/null @@ -1,336 +0,0 @@ -#include "group_keys.h" -#include "group_info.h" -#include "group_members.h" - -#include "jni_utils.h" - -extern "C" -JNIEXPORT jint JNICALL - Java_network_loki_messenger_libsession_1util_GroupKeysConfig_00024Companion_storageNamespace(JNIEnv* env, - jobject thiz) { - return (jint)session::config::Namespace::GroupKeys; -} - -extern "C" -JNIEXPORT jlong JNICALL -Java_network_loki_messenger_libsession_1util_GroupKeysConfig_00024Companion_newInstance(JNIEnv *env, - jobject thiz, - jbyteArray user_secret_key, - jbyteArray group_public_key, - jbyteArray group_secret_key, - jbyteArray initial_dump, - jlong info_pointer, - jlong members_pointer) { - return jni_utils::run_catching_cxx_exception_or_throws(env, [=] { - std::lock_guard lock{util::util_mutex_}; - auto user_key_bytes = util::ustring_from_bytes(env, user_secret_key); - auto pub_key_bytes = util::ustring_from_bytes(env, group_public_key); - std::optional secret_key_optional{std::nullopt}; - std::optional initial_dump_optional{std::nullopt}; - - if (group_secret_key && env->GetArrayLength(group_secret_key) > 0) { - auto secret_key_bytes = util::ustring_from_bytes(env, group_secret_key); - secret_key_optional = secret_key_bytes; - } - - if (initial_dump && env->GetArrayLength(initial_dump) > 0) { - auto initial_dump_bytes = util::ustring_from_bytes(env, initial_dump); - initial_dump_optional = initial_dump_bytes; - } - - auto info = reinterpret_cast(info_pointer); - auto members = reinterpret_cast(members_pointer); - - auto* keys = new session::config::groups::Keys(user_key_bytes, - pub_key_bytes, - secret_key_optional, - initial_dump_optional, - *info, - *members); - - return reinterpret_cast(keys); - }); -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_GroupKeysConfig_groupKeys(JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto config = ptrToKeys(env, thiz); - auto keys = config->group_keys(); - jclass stack = env->FindClass("java/util/Stack"); - jmethodID init = env->GetMethodID(stack, "", "()V"); - jobject our_stack = env->NewObject(stack, init); - jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); - for (auto& key : keys) { - auto key_bytes = util::bytes_from_ustring(env, key.data()); - env->CallObjectMethod(our_stack, push, key_bytes); - } - return our_stack; -} - -extern "C" -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_GroupKeysConfig_loadKey(JNIEnv *env, jobject thiz, - jbyteArray message, - jstring hash, - jlong timestamp_ms, - jlong info_ptr, - jlong members_ptr) { - std::lock_guard lock{util::util_mutex_}; - auto keys = ptrToKeys(env, thiz); - auto message_bytes = util::ustring_from_bytes(env, message); - auto hash_bytes = env->GetStringUTFChars(hash, nullptr); - auto info = reinterpret_cast(info_ptr); - auto members = reinterpret_cast(members_ptr); - - auto processed = jni_utils::run_catching_cxx_exception_or_throws(env, [&] { - return keys->load_key_message(hash_bytes, message_bytes, timestamp_ms, *info, *members); - }); - - env->ReleaseStringUTFChars(hash, hash_bytes); - return processed; -} - -extern "C" -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_GroupKeysConfig_needsRekey(JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto keys = ptrToKeys(env, thiz); - return keys->needs_rekey(); -} - -extern "C" -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_GroupKeysConfig_needsDump(JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto keys = ptrToKeys(env, thiz); - return keys->needs_dump(); -} - - - -extern "C" -JNIEXPORT jbyteArray JNICALL -Java_network_loki_messenger_libsession_1util_GroupKeysConfig_pendingKey(JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto keys = ptrToKeys(env, thiz); - auto pending = keys->pending_key(); - if (!pending) { - return nullptr; - } - auto pending_bytes = util::bytes_from_ustring(env, *pending); - return pending_bytes; -} - -extern "C" -JNIEXPORT jbyteArray JNICALL -Java_network_loki_messenger_libsession_1util_GroupKeysConfig_pendingConfig(JNIEnv *env, - jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto keys = ptrToKeys(env, thiz); - auto pending = keys->pending_config(); - if (!pending) { - return nullptr; - } - auto pending_bytes = util::bytes_from_ustring(env, *pending); - return pending_bytes; -} - -extern "C" -JNIEXPORT jbyteArray JNICALL -Java_network_loki_messenger_libsession_1util_GroupKeysConfig_rekey(JNIEnv *env, jobject thiz, - jlong info_ptr, jlong members_ptr) { - std::lock_guard lock{util::util_mutex_}; - auto keys = ptrToKeys(env, thiz); - auto info = reinterpret_cast(info_ptr); - auto members = reinterpret_cast(members_ptr); - auto rekey = keys->rekey(*info, *members); - auto rekey_bytes = util::bytes_from_ustring(env, rekey.data()); - return rekey_bytes; -} - -extern "C" -JNIEXPORT jbyteArray JNICALL -Java_network_loki_messenger_libsession_1util_GroupKeysConfig_dump(JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto keys = ptrToKeys(env, thiz); - auto dump = keys->dump(); - auto byte_array = util::bytes_from_ustring(env, dump); - return byte_array; -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_GroupKeysConfig_free(JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto ptr = ptrToKeys(env, thiz); - delete ptr; -} - -extern "C" -JNIEXPORT jbyteArray JNICALL -Java_network_loki_messenger_libsession_1util_GroupKeysConfig_encrypt(JNIEnv *env, jobject thiz, - jbyteArray plaintext) { - return jni_utils::run_catching_cxx_exception_or_throws(env, [=] { - std::lock_guard lock{util::util_mutex_}; - auto ptr = ptrToKeys(env, thiz); - auto plaintext_ustring = util::ustring_from_bytes(env, plaintext); - auto enc = ptr->encrypt_message(plaintext_ustring); - return util::bytes_from_ustring(env, enc); - }); -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_GroupKeysConfig_decrypt(JNIEnv *env, jobject thiz, - jbyteArray ciphertext) { - return jni_utils::run_catching_cxx_exception_or_throws(env, [=] { - std::lock_guard lock{util::util_mutex_}; - auto ptr = ptrToKeys(env, thiz); - auto ciphertext_ustring = util::ustring_from_bytes(env, ciphertext); - auto decrypted = ptr->decrypt_message(ciphertext_ustring); - auto sender = decrypted.first; - auto plaintext = decrypted.second; - auto plaintext_bytes = util::bytes_from_ustring(env, plaintext); - auto sender_session_id = util::serialize_account_id(env, sender.data()); - auto pair_class = env->FindClass("kotlin/Pair"); - auto pair_constructor = env->GetMethodID(pair_class, "", "(Ljava/lang/Object;Ljava/lang/Object;)V"); - auto pair_obj = env->NewObject(pair_class, pair_constructor, plaintext_bytes, sender_session_id); - return pair_obj; - }); -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_GroupKeysConfig_keys(JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto ptr = ptrToKeys(env, thiz); - auto keys = ptr->group_keys(); - jclass stack = env->FindClass("java/util/Stack"); - jmethodID init = env->GetMethodID(stack, "", "()V"); - jobject our_stack = env->NewObject(stack, init); - jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); - for (auto& key : keys) { - auto key_bytes = util::bytes_from_ustring(env, key); - env->CallObjectMethod(our_stack, push, key_bytes); - } - return our_stack; -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_GroupKeysConfig_currentHashes(JNIEnv *env, - jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto ptr = ptrToKeys(env, thiz); - auto existing = ptr->current_hashes(); - jclass stack = env->FindClass("java/util/Stack"); - jmethodID init = env->GetMethodID(stack, "", "()V"); - jobject our_list = env->NewObject(stack, init); - jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); - for (auto& hash : existing) { - auto hash_bytes = env->NewStringUTF(hash.data()); - env->CallObjectMethod(our_list, push, hash_bytes); - } - return our_list; -} -extern "C" -JNIEXPORT jbyteArray JNICALL -Java_network_loki_messenger_libsession_1util_GroupKeysConfig_makeSubAccount(JNIEnv *env, - jobject thiz, - jobject session_id, - jboolean can_write, - jboolean can_delete) { - std::lock_guard lock{util::util_mutex_}; - auto ptr = ptrToKeys(env, thiz); - auto deserialized_id = util::deserialize_account_id(env, session_id); - auto new_subaccount_key = ptr->swarm_make_subaccount(deserialized_id.data(), can_write, can_delete); - auto jbytes = util::bytes_from_ustring(env, new_subaccount_key); - return jbytes; -} - -extern "C" -JNIEXPORT jbyteArray JNICALL -Java_network_loki_messenger_libsession_1util_GroupKeysConfig_getSubAccountToken(JNIEnv *env, - jobject thiz, - jobject session_id, - jboolean can_write, - jboolean can_delete) { - std::lock_guard lock{util::util_mutex_}; - auto ptr = ptrToKeys(env, thiz); - auto deserialized_id = util::deserialize_account_id(env, session_id); - auto token = ptr->swarm_subaccount_token(deserialized_id, can_write, can_delete); - auto jbytes = util::bytes_from_ustring(env, token); - return jbytes; -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_GroupKeysConfig_subAccountSign(JNIEnv *env, - jobject thiz, - jbyteArray message, - jbyteArray signing_value) { - std::lock_guard lock{util::util_mutex_}; - auto ptr = ptrToKeys(env, thiz); - auto message_ustring = util::ustring_from_bytes(env, message); - auto signing_value_ustring = util::ustring_from_bytes(env, signing_value); - auto swarm_auth = ptr->swarm_subaccount_sign(message_ustring, signing_value_ustring, false); - return util::deserialize_swarm_auth(env, swarm_auth); -} - -extern "C" -JNIEXPORT jbyteArray JNICALL -Java_network_loki_messenger_libsession_1util_GroupKeysConfig_supplementFor(JNIEnv *env, - jobject thiz, - jobjectArray j_user_session_ids) { - std::lock_guard lock{util::util_mutex_}; - auto ptr = ptrToKeys(env, thiz); - std::vector user_session_ids; - for (int i = 0, size = env->GetArrayLength(j_user_session_ids); i < size; i++) { - user_session_ids.push_back(util::string_from_jstring(env, (jstring)(env->GetObjectArrayElement(j_user_session_ids, i)))); - } - auto supplement = ptr->key_supplement(user_session_ids); - return util::bytes_from_ustring(env, supplement); -} -extern "C" -JNIEXPORT jint JNICALL -Java_network_loki_messenger_libsession_1util_GroupKeysConfig_currentGeneration(JNIEnv *env, - jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto ptr = ptrToKeys(env, thiz); - return ptr->current_generation(); -} - -extern "C" -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_GroupKeysConfig_admin(JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto ptr = ptrToKeys(env, thiz); - return ptr->admin(); -} - -extern "C" -JNIEXPORT jint JNICALL -Java_network_loki_messenger_libsession_1util_GroupKeysConfig_size(JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto ptr = ptrToKeys(env, thiz); - return ptr->size(); -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_GroupKeysConfig_loadAdminKey(JNIEnv *env, jobject thiz, - jbyteArray admin_key, - jlong info_ptr, - jlong members_ptr) { - std::lock_guard lock{util::util_mutex_}; - auto ptr = ptrToKeys(env, thiz); - auto admin_key_ustring = util::ustring_from_bytes(env, admin_key); - auto info = reinterpret_cast(info_ptr); - auto members = reinterpret_cast(members_ptr); - - jni_utils::run_catching_cxx_exception_or_throws(env, [&] { - ptr->load_admin_key(admin_key_ustring, *info, *members); - }); -} diff --git a/libsession-util/src/main/cpp/group_keys.h b/libsession-util/src/main/cpp/group_keys.h deleted file mode 100644 index 9958932c1d2..00000000000 --- a/libsession-util/src/main/cpp/group_keys.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef SESSION_ANDROID_GROUP_KEYS_H -#define SESSION_ANDROID_GROUP_KEYS_H - -#include "util.h" - -inline session::config::groups::Keys* ptrToKeys(JNIEnv* env, jobject obj) { - jclass configClass = env->FindClass("network/loki/messenger/libsession_util/GroupKeysConfig"); - jfieldID pointerField = env->GetFieldID(configClass, "pointer", "J"); - return (session::config::groups::Keys*) env->GetLongField(obj, pointerField); -} - -#endif //SESSION_ANDROID_GROUP_KEYS_H diff --git a/libsession-util/src/main/cpp/group_members.cpp b/libsession-util/src/main/cpp/group_members.cpp deleted file mode 100644 index 7d4dd58cf26..00000000000 --- a/libsession-util/src/main/cpp/group_members.cpp +++ /dev/null @@ -1,240 +0,0 @@ -#include "group_members.h" - -#include "jni_utils.h" - -extern "C" -JNIEXPORT jlong JNICALL -Java_network_loki_messenger_libsession_1util_GroupMembersConfig_00024Companion_newInstance( - JNIEnv *env, jobject thiz, jbyteArray pub_key, jbyteArray secret_key, - jbyteArray initial_dump) { - std::lock_guard lock{util::util_mutex_}; - auto pub_key_bytes = util::ustring_from_bytes(env, pub_key); - std::optional secret_key_optional{std::nullopt}; - std::optional initial_dump_optional{std::nullopt}; - if (secret_key && env->GetArrayLength(secret_key) > 0) { - auto secret_key_bytes = util::ustring_from_bytes(env, secret_key); - secret_key_optional = secret_key_bytes; - } - if (initial_dump && env->GetArrayLength(initial_dump) > 0) { - auto initial_dump_bytes = util::ustring_from_bytes(env, initial_dump); - initial_dump_optional = initial_dump_bytes; - } - - auto* group_members = new session::config::groups::Members(pub_key_bytes, secret_key_optional, initial_dump_optional); - return reinterpret_cast(group_members); -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_GroupMembersConfig_all(JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto config = ptrToMembers(env, thiz); - jclass stack = env->FindClass("java/util/Stack"); - jmethodID init = env->GetMethodID(stack, "", "()V"); - jobject our_stack = env->NewObject(stack, init); - jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); - for (auto& member : *config) { - auto member_obj = util::serialize_group_member(env, member); - env->CallObjectMethod(our_stack, push, member_obj); - } - return our_stack; -} - -extern "C" -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_GroupMembersConfig_erase(JNIEnv *env, jobject thiz, jstring pub_key_hex) { - auto config = ptrToMembers(env, thiz); - auto member_id = env->GetStringUTFChars(pub_key_hex, nullptr); - auto erased = config->erase(member_id); - env->ReleaseStringUTFChars(pub_key_hex, member_id); - return erased; -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_GroupMembersConfig_get(JNIEnv *env, jobject thiz, - jstring pub_key_hex) { - return jni_utils::run_catching_cxx_exception_or_throws(env, [=]() -> jobject { - std::lock_guard lock{util::util_mutex_}; - auto config = ptrToMembers(env, thiz); - auto pub_key_bytes = env->GetStringUTFChars(pub_key_hex, nullptr); - auto member = config->get(pub_key_bytes); - if (!member) { - return nullptr; - } - auto serialized = util::serialize_group_member(env, *member); - env->ReleaseStringUTFChars(pub_key_hex, pub_key_bytes); - return serialized; - }); -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_GroupMembersConfig_getOrConstruct(JNIEnv *env, - jobject thiz, - jstring pub_key_hex) { - std::lock_guard lock{util::util_mutex_}; - auto config = ptrToMembers(env, thiz); - auto pub_key_bytes = env->GetStringUTFChars(pub_key_hex, nullptr); - auto member = config->get_or_construct(pub_key_bytes); - auto serialized = util::serialize_group_member(env, member); - env->ReleaseStringUTFChars(pub_key_hex, pub_key_bytes); - return serialized; -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_GroupMembersConfig_set(JNIEnv *env, jobject thiz, - jobject group_member) { - std::lock_guard lock{util::util_mutex_}; - ptrToMembers(env, thiz)->set(*ptrToMember(env, group_member)); -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_util_GroupMember_setInvited(JNIEnv *env, - jobject thiz) { - ptrToMember(env, thiz)->invite_status = session::config::groups::STATUS_NOT_SENT; -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_util_GroupMember_setInviteSent(JNIEnv *env, - jobject thiz) { - ptrToMember(env, thiz)->set_invite_sent(); -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_util_GroupMember_setInviteFailed(JNIEnv *env, - jobject thiz) { - ptrToMember(env, thiz)->set_invite_failed(); -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_util_GroupMember_setInviteAccepted(JNIEnv *env, - jobject thiz) { - ptrToMember(env, thiz)->set_invite_accepted(); -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_util_GroupMember_setPromoted(JNIEnv *env, - jobject thiz) { - ptrToMember(env, thiz)->set_promoted(); -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_util_GroupMember_setPromotionSent(JNIEnv *env, - jobject thiz) { - ptrToMember(env, thiz)->set_promotion_sent(); -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_util_GroupMember_setPromotionFailed(JNIEnv *env, - jobject thiz) { - ptrToMember(env, thiz)->set_promotion_failed(); -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_util_GroupMember_setPromotionAccepted(JNIEnv *env, - jobject thiz) { - ptrToMember(env, thiz)->set_promotion_accepted(); -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_util_GroupMember_setRemoved(JNIEnv *env, jobject thiz, - jboolean also_remove_messages) { - ptrToMember(env, thiz)->set_removed(also_remove_messages); -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_util_GroupMember_setName(JNIEnv *env, jobject thiz, - jstring name) { - auto name_bytes = env->GetStringUTFChars(name, nullptr); - ptrToMember(env, thiz)->set_name(name_bytes); - env->ReleaseStringUTFChars(name, name_bytes); -} - -extern "C" -JNIEXPORT jstring JNICALL -Java_network_loki_messenger_libsession_1util_util_GroupMember_nameString(JNIEnv *env, - jobject thiz) { - return util::jstringFromOptional(env, ptrToMember(env, thiz)->name); -} - -extern "C" -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_util_GroupMember_isAdmin(JNIEnv *env, jobject thiz) { - return ptrToMember(env, thiz)->admin; -} - -extern "C" -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_util_GroupMember_isSupplement(JNIEnv *env, - jobject thiz) { - return ptrToMember(env, thiz)->supplement; -} - -extern "C" -JNIEXPORT jstring JNICALL -Java_network_loki_messenger_libsession_1util_util_GroupMember_accountIdString(JNIEnv *env, - jobject thiz) { - return util::jstringFromOptional(env, ptrToMember(env, thiz)->session_id); -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_util_GroupMember_destroy(JNIEnv *env, jobject thiz) { - delete ptrToMember(env, thiz); -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_util_GroupMember_profilePic(JNIEnv *env, - jobject thiz) { - return util::serialize_user_pic(env, ptrToMember(env, thiz)->profile_picture); -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_util_GroupMember_setProfilePic(JNIEnv *env, - jobject thiz, - jobject pic) { - const auto [jurl, jkey] = util::deserialize_user_pic(env, pic); - auto url = util::string_from_jstring(env, jurl); - auto key = util::ustring_from_bytes(env, jkey); - auto &picture = ptrToMember(env, thiz)->profile_picture; - picture.url = url; - picture.key = key; -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_util_GroupMember_setSupplement(JNIEnv *env, - jobject thiz, - jboolean supplement) { - ptrToMember(env, thiz)->supplement = supplement; -} - - -extern "C" -JNIEXPORT jint JNICALL -Java_network_loki_messenger_libsession_1util_GroupMembersConfig_statusInt(JNIEnv *env, jobject thiz, - jobject group_member) { - return static_cast(ptrToMembers(env, thiz)->get_status(*ptrToMember(env, group_member))); -} -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_GroupMembersConfig_setPendingSend(JNIEnv *env, - jobject thiz, - jstring pub_key_hex, - jboolean pending) { - ptrToMembers(env, thiz)->set_pending_send(util::string_from_jstring(env, pub_key_hex), pending); -} \ No newline at end of file diff --git a/libsession-util/src/main/cpp/group_members.h b/libsession-util/src/main/cpp/group_members.h deleted file mode 100644 index 38e11429435..00000000000 --- a/libsession-util/src/main/cpp/group_members.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef SESSION_ANDROID_GROUP_MEMBERS_H -#define SESSION_ANDROID_GROUP_MEMBERS_H - -#include "util.h" - -inline session::config::groups::Members* ptrToMembers(JNIEnv* env, jobject obj) { - jclass configClass = env->FindClass("network/loki/messenger/libsession_util/GroupMembersConfig"); - jfieldID pointerField = env->GetFieldID(configClass, "pointer", "J"); - return (session::config::groups::Members*) env->GetLongField(obj, pointerField); -} - -inline session::config::groups::member *ptrToMember(JNIEnv *env, jobject thiz) { - auto ptrField = env->GetFieldID(env->GetObjectClass(thiz), "nativePtr", "J"); - return reinterpret_cast(env->GetLongField(thiz, ptrField)); -} - - -#endif //SESSION_ANDROID_GROUP_MEMBERS_H diff --git a/libsession-util/src/main/cpp/jni_utils.h b/libsession-util/src/main/cpp/jni_utils.h deleted file mode 100644 index c9ccd924a65..00000000000 --- a/libsession-util/src/main/cpp/jni_utils.h +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef SESSION_ANDROID_JNI_UTILS_H -#define SESSION_ANDROID_JNI_UTILS_H - -#include -#include - -namespace jni_utils { - /** - * Run a C++ function and catch any exceptions, throwing a Java exception if one is caught, - * and returning a default-constructed value of the specified type. - * - * @tparam RetT The return type of the function - * @tparam Func The function type - * @param f The function to run - * @param fallbackRun The function to run if an exception is caught. The optional exception message reference will be passed to this function. - * @return The return value of the function, or the return value of the fallback function if an exception was caught - */ - template - RetT run_catching_cxx_exception_or(Func f, FallbackRun fallbackRun) { - try { - return f(); - } catch (const std::exception &e) { - return fallbackRun(e.what()); - } catch (...) { - return fallbackRun(nullptr); - } - } - - /** - * Run a C++ function and catch any exceptions, throwing a Java exception if one is caught. - * - * @tparam RetT The return type of the function - * @tparam Func The function type - * @param env The JNI environment - * @param f The function to run - * @return The return value of the function, or a default-constructed value of the specified type if an exception was caught - */ - template - RetT run_catching_cxx_exception_or_throws(JNIEnv *env, Func f) { - return run_catching_cxx_exception_or(f, [env](const char *msg) { - jclass exceptionClass = env->FindClass("java/lang/RuntimeException"); - if (msg) { - auto formatted_message = std::string("libsession: C++ exception: ") + msg; - env->ThrowNew(exceptionClass, formatted_message.c_str()); - } else { - env->ThrowNew(exceptionClass, "libsession: Unknown C++ exception"); - } - - return RetT(); - }); - } -} - -#endif //SESSION_ANDROID_JNI_UTILS_H diff --git a/libsession-util/src/main/cpp/logging.cpp b/libsession-util/src/main/cpp/logging.cpp deleted file mode 100644 index 3f39b4ac782..00000000000 --- a/libsession-util/src/main/cpp/logging.cpp +++ /dev/null @@ -1,47 +0,0 @@ -#include -#include -#include -#include - -#include "session/logging.hpp" -#include "session/log_level.h" - -#define LOG_TAG "LibSession" - -extern "C" JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_util_Logger_initLogger(JNIEnv* env, jclass clazz) { - session::add_logger([](std::string_view msg, std::string_view category, session::LogLevel level) { - android_LogPriority prio = ANDROID_LOG_VERBOSE; - - switch (level.level) { - case LOG_LEVEL_TRACE: - prio = ANDROID_LOG_VERBOSE; - break; - - case LOG_LEVEL_DEBUG: - prio = ANDROID_LOG_DEBUG; - break; - - case LOG_LEVEL_INFO: - prio = ANDROID_LOG_INFO; - break; - - case LOG_LEVEL_WARN: - prio = ANDROID_LOG_WARN; - break; - - case LOG_LEVEL_ERROR: - case LOG_LEVEL_CRITICAL: - prio = ANDROID_LOG_ERROR; - break; - - default: - prio = ANDROID_LOG_INFO; - break; - } - - __android_log_print(prio, LOG_TAG, "%.*s [%.*s]", - static_cast(msg.size()), msg.data(), - static_cast(category.size()), category.data()); -}); -} \ No newline at end of file diff --git a/libsession-util/src/main/cpp/user_groups.cpp b/libsession-util/src/main/cpp/user_groups.cpp deleted file mode 100644 index b34d56e8251..00000000000 --- a/libsession-util/src/main/cpp/user_groups.cpp +++ /dev/null @@ -1,346 +0,0 @@ -#include "user_groups.h" -#include "oxenc/hex.h" - -#include "session/ed25519.hpp" - -extern "C" -JNIEXPORT jint JNICALL -Java_network_loki_messenger_libsession_1util_util_GroupInfo_00024LegacyGroupInfo_00024Companion_NAME_1MAX_1LENGTH( - JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - return session::config::legacy_group_info::NAME_MAX_LENGTH; -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_UserGroupsConfig_getCommunityInfo(JNIEnv *env, - jobject thiz, - jstring base_url, - jstring room) { - std::lock_guard lock{util::util_mutex_}; - auto conf = ptrToUserGroups(env, thiz); - auto base_url_bytes = env->GetStringUTFChars(base_url, nullptr); - auto room_bytes = env->GetStringUTFChars(room, nullptr); - - auto community = conf->get_community(base_url_bytes, room_bytes); - - jobject community_info = nullptr; - - if (community) { - community_info = serialize_community_info(env, *community); - } - env->ReleaseStringUTFChars(base_url, base_url_bytes); - env->ReleaseStringUTFChars(room, room_bytes); - return community_info; -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_UserGroupsConfig_getLegacyGroupInfo(JNIEnv *env, - jobject thiz, - jstring account_id) { - std::lock_guard lock{util::util_mutex_}; - auto conf = ptrToUserGroups(env, thiz); - auto id_bytes = env->GetStringUTFChars(account_id, nullptr); - auto legacy_group = conf->get_legacy_group(id_bytes); - jobject return_group = nullptr; - if (legacy_group) { - return_group = serialize_legacy_group_info(env, *legacy_group); - } - env->ReleaseStringUTFChars(account_id, id_bytes); - return return_group; -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_UserGroupsConfig_getOrConstructCommunityInfo( - JNIEnv *env, jobject thiz, jstring base_url, jstring room, jstring pub_key_hex) { - std::lock_guard lock{util::util_mutex_}; - auto conf = ptrToUserGroups(env, thiz); - auto base_url_bytes = env->GetStringUTFChars(base_url, nullptr); - auto room_bytes = env->GetStringUTFChars(room, nullptr); - auto pub_hex_bytes = env->GetStringUTFChars(pub_key_hex, nullptr); - - auto group = conf->get_or_construct_community(base_url_bytes, room_bytes, pub_hex_bytes); - - env->ReleaseStringUTFChars(base_url, base_url_bytes); - env->ReleaseStringUTFChars(room, room_bytes); - env->ReleaseStringUTFChars(pub_key_hex, pub_hex_bytes); - return serialize_community_info(env, group); -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_UserGroupsConfig_getOrConstructLegacyGroupInfo( - JNIEnv *env, jobject thiz, jstring account_id) { - std::lock_guard lock{util::util_mutex_}; - auto conf = ptrToUserGroups(env, thiz); - auto id_bytes = env->GetStringUTFChars(account_id, nullptr); - auto group = conf->get_or_construct_legacy_group(id_bytes); - env->ReleaseStringUTFChars(account_id, id_bytes); - return serialize_legacy_group_info(env, group); -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_UserGroupsConfig_set__Lnetwork_loki_messenger_libsession_1util_util_GroupInfo_2( - JNIEnv *env, jobject thiz, jobject group_info) { - std::lock_guard lock{util::util_mutex_}; - auto conf = ptrToUserGroups(env, thiz); - auto community_info = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$CommunityGroupInfo"); - auto legacy_info = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$LegacyGroupInfo"); - auto closed_group_info = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$ClosedGroupInfo"); - - auto object_class = env->GetObjectClass(group_info); - if (env->IsSameObject(community_info, object_class)) { - auto deserialized = deserialize_community_info(env, group_info, conf); - conf->set(deserialized); - } else if (env->IsSameObject(legacy_info, object_class)) { - auto deserialized = deserialize_legacy_group_info(env, group_info, conf); - conf->set(deserialized); - } else if (env->IsSameObject(closed_group_info, object_class)) { - auto deserialized = deserialize_closed_group_info(env, group_info); - conf->set(deserialized); - } -} - - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_UserGroupsConfig_erase__Lnetwork_loki_messenger_libsession_1util_util_GroupInfo_2( - JNIEnv *env, jobject thiz, jobject group_info) { - std::lock_guard lock{util::util_mutex_}; - auto conf = ptrToUserGroups(env, thiz); - auto communityInfo = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$CommunityGroupInfo"); - auto legacyInfo = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$LegacyGroupInfo"); - auto closedGroupInfo = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$ClosedGroupInfo"); - auto object_class = env->GetObjectClass(group_info); - if (env->IsSameObject(communityInfo, object_class)) { - auto deserialized = deserialize_community_info(env, group_info, conf); - conf->erase(deserialized); - } else if (env->IsSameObject(legacyInfo, object_class)) { - auto deserialized = deserialize_legacy_group_info(env, group_info, conf); - conf->erase(deserialized); - } else if (env->IsSameObject(closedGroupInfo, object_class)) { - auto deserialized = deserialize_closed_group_info(env, group_info); - conf->erase(deserialized); - } -} - -extern "C" -JNIEXPORT jlong JNICALL -Java_network_loki_messenger_libsession_1util_UserGroupsConfig_sizeCommunityInfo(JNIEnv *env, - jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto conf = ptrToUserGroups(env, thiz); - return conf->size_communities(); -} - -extern "C" -JNIEXPORT jlong JNICALL -Java_network_loki_messenger_libsession_1util_UserGroupsConfig_sizeLegacyGroupInfo(JNIEnv *env, - jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto conf = ptrToUserGroups(env, thiz); - return conf->size_legacy_groups(); -} - -extern "C" -JNIEXPORT jlong JNICALL -Java_network_loki_messenger_libsession_1util_UserGroupsConfig_size(JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto conf = ptrToConvoInfo(env, thiz); - return conf->size(); -} - -inline jobject iterator_as_java_stack(JNIEnv *env, const session::config::UserGroups::iterator& begin, const session::config::UserGroups::iterator& end) { - jclass stack = env->FindClass("java/util/Stack"); - jmethodID init = env->GetMethodID(stack, "", "()V"); - jobject our_stack = env->NewObject(stack, init); - jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); - for (auto it = begin; it != end;) { - // do something with it - auto item = *it; - jobject serialized = nullptr; - if (auto* lgc = std::get_if(&item)) { - serialized = serialize_legacy_group_info(env, *lgc); - } else if (auto* community = std::get_if(&item)) { - serialized = serialize_community_info(env, *community); - } else if (auto* closed = std::get_if(&item)) { - serialized = serialize_closed_group_info(env, *closed); - } - if (serialized != nullptr) { - env->CallObjectMethod(our_stack, push, serialized); - } - it++; - } - return our_stack; -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_UserGroupsConfig_all(JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto conf = ptrToUserGroups(env, thiz); - jobject all_stack = iterator_as_java_stack(env, conf->begin(), conf->end()); - return all_stack; -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_UserGroupsConfig_allCommunityInfo(JNIEnv *env, - jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto conf = ptrToUserGroups(env, thiz); - jobject community_stack = iterator_as_java_stack(env, conf->begin_communities(), conf->end()); - return community_stack; -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_UserGroupsConfig_allLegacyGroupInfo(JNIEnv *env, - jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto conf = ptrToUserGroups(env, thiz); - jobject legacy_stack = iterator_as_java_stack(env, conf->begin_legacy_groups(), conf->end()); - return legacy_stack; -} - -extern "C" -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_UserGroupsConfig_eraseCommunity__Lnetwork_loki_messenger_libsession_1util_util_BaseCommunityInfo_2(JNIEnv *env, - jobject thiz, - jobject base_community_info) { - std::lock_guard lock{util::util_mutex_}; - auto conf = ptrToUserGroups(env, thiz); - auto base_community = util::deserialize_base_community(env, base_community_info); - return conf->erase_community(base_community.base_url(),base_community.room()); -} - -extern "C" -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_UserGroupsConfig_eraseCommunity__Ljava_lang_String_2Ljava_lang_String_2( - JNIEnv *env, jobject thiz, jstring server, jstring room) { - std::lock_guard lock{util::util_mutex_}; - auto conf = ptrToUserGroups(env, thiz); - auto server_bytes = env->GetStringUTFChars(server, nullptr); - auto room_bytes = env->GetStringUTFChars(room, nullptr); - auto community = conf->get_community(server_bytes, room_bytes); - bool deleted = false; - if (community) { - deleted = conf->erase(*community); - } - env->ReleaseStringUTFChars(server, server_bytes); - env->ReleaseStringUTFChars(room, room_bytes); - return deleted; -} - -extern "C" -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_UserGroupsConfig_eraseLegacyGroup(JNIEnv *env, - jobject thiz, - jstring account_id) { - std::lock_guard lock{util::util_mutex_}; - auto conf = ptrToUserGroups(env, thiz); - auto account_id_bytes = env->GetStringUTFChars(account_id, nullptr); - bool return_bool = conf->erase_legacy_group(account_id_bytes); - env->ReleaseStringUTFChars(account_id, account_id_bytes); - return return_bool; -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_UserGroupsConfig_getClosedGroup(JNIEnv *env, - jobject thiz, - jstring session_id) { - std::lock_guard guard{util::util_mutex_}; - auto config = ptrToUserGroups(env, thiz); - auto session_id_bytes = env->GetStringUTFChars(session_id, nullptr); - - auto group = config->get_group(session_id_bytes); - - env->ReleaseStringUTFChars(session_id, session_id_bytes); - - if (group) { - return serialize_closed_group_info(env, *group); - } - return nullptr; -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_UserGroupsConfig_getOrConstructClosedGroup(JNIEnv *env, - jobject thiz, - jstring session_id) { - std::lock_guard guard{util::util_mutex_}; - auto config = ptrToUserGroups(env, thiz); - auto session_id_bytes = env->GetStringUTFChars(session_id, nullptr); - - auto group = config->get_or_construct_group(session_id_bytes); - - env->ReleaseStringUTFChars(session_id, session_id_bytes); - - return serialize_closed_group_info(env, group); -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_UserGroupsConfig_allClosedGroupInfo(JNIEnv *env, - jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto conf = ptrToUserGroups(env, thiz); - auto closed_group_stack = iterator_as_java_stack(env, conf->begin_groups(), conf->end()); - - return closed_group_stack; -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_UserGroupsConfig_createGroup(JNIEnv *env, - jobject thiz) { - std::lock_guard guard{util::util_mutex_}; - auto config = ptrToUserGroups(env, thiz); - - auto group = config->create_group(); - return serialize_closed_group_info(env, group); -} - -extern "C" -JNIEXPORT jlong JNICALL - Java_network_loki_messenger_libsession_1util_UserGroupsConfig_sizeClosedGroup(JNIEnv *env, - jobject thiz) { - std::lock_guard guard{util::util_mutex_}; - auto config = ptrToUserGroups(env, thiz); - return config->size_groups(); -} - -extern "C" -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_UserGroupsConfig_eraseClosedGroup(JNIEnv *env, - jobject thiz, - jstring session_id) { - std::lock_guard guard{util::util_mutex_}; - auto config = ptrToUserGroups(env, thiz); - auto session_id_bytes = env->GetStringUTFChars(session_id, nullptr); - bool return_value = config->erase_group(session_id_bytes); - env->ReleaseStringUTFChars(session_id, session_id_bytes); - return return_value; -} - -extern "C" -JNIEXPORT jbyteArray JNICALL -Java_network_loki_messenger_libsession_1util_util_GroupInfo_00024ClosedGroupInfo_adminKeyFromSeed( - JNIEnv *env, jclass clazz, jbyteArray seed) { - auto len = env->GetArrayLength(seed); - if (len != 32) { - env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"), "Seed must be 32 bytes"); - return nullptr; - } - - auto seed_bytes = env->GetByteArrayElements(seed, nullptr); - auto admin_key = session::ed25519::ed25519_key_pair( - session::ustring_view(reinterpret_cast(seed_bytes), 32)).second; - env->ReleaseByteArrayElements(seed, seed_bytes, 0); - - return util::bytes_from_ustring(env, session::ustring_view(admin_key.data(), admin_key.size())); -} \ No newline at end of file diff --git a/libsession-util/src/main/cpp/user_groups.h b/libsession-util/src/main/cpp/user_groups.h deleted file mode 100644 index 265af68013a..00000000000 --- a/libsession-util/src/main/cpp/user_groups.h +++ /dev/null @@ -1,194 +0,0 @@ - -#ifndef SESSION_ANDROID_USER_GROUPS_H -#define SESSION_ANDROID_USER_GROUPS_H - -#include "jni.h" -#include "util.h" -#include "conversation.h" -#include "session/config/user_groups.hpp" - -inline session::config::UserGroups* ptrToUserGroups(JNIEnv *env, jobject obj) { - jclass configClass = env->FindClass("network/loki/messenger/libsession_util/UserGroupsConfig"); - jfieldID pointerField = env->GetFieldID(configClass, "pointer", "J"); - return (session::config::UserGroups*) env->GetLongField(obj, pointerField); -} - -inline void deserialize_members_into(JNIEnv *env, jobject members_map, session::config::legacy_group_info& to_append_group) { - jclass map_class = env->FindClass("java/util/Map"); - jclass map_entry_class = env->FindClass("java/util/Map$Entry"); - jclass set_class = env->FindClass("java/util/Set"); - jclass iterator_class = env->FindClass("java/util/Iterator"); - jclass boxed_bool = env->FindClass("java/lang/Boolean"); - - jmethodID get_entry_set = env->GetMethodID(map_class, "entrySet", "()Ljava/util/Set;"); - jmethodID get_at = env->GetMethodID(set_class, "iterator", "()Ljava/util/Iterator;"); - jmethodID has_next = env->GetMethodID(iterator_class, "hasNext", "()Z"); - jmethodID next = env->GetMethodID(iterator_class, "next", "()Ljava/lang/Object;"); - jmethodID get_key = env->GetMethodID(map_entry_class, "getKey", "()Ljava/lang/Object;"); - jmethodID get_value = env->GetMethodID(map_entry_class, "getValue", "()Ljava/lang/Object;"); - jmethodID get_bool_value = env->GetMethodID(boxed_bool, "booleanValue", "()Z"); - - jobject entry_set = env->CallObjectMethod(members_map, get_entry_set); - jobject iterator = env->CallObjectMethod(entry_set, get_at); - - while (env->CallBooleanMethod(iterator, has_next)) { - jobject entry = env->CallObjectMethod(iterator, next); - jstring key = static_cast(env->CallObjectMethod(entry, get_key)); - jobject boxed = env->CallObjectMethod(entry, get_value); - bool is_admin = env->CallBooleanMethod(boxed, get_bool_value); - auto member_string = env->GetStringUTFChars(key, nullptr); - to_append_group.insert(member_string, is_admin); - env->ReleaseStringUTFChars(key, member_string); - } -} - -inline session::config::legacy_group_info deserialize_legacy_group_info(JNIEnv *env, jobject info, session::config::UserGroups* conf) { - auto clazz = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$LegacyGroupInfo"); - auto id_field = env->GetFieldID(clazz, "accountId", "Ljava/lang/String;"); - auto name_field = env->GetFieldID(clazz, "name", "Ljava/lang/String;"); - auto members_field = env->GetFieldID(clazz, "members", "Ljava/util/Map;"); - auto enc_pub_key_field = env->GetFieldID(clazz, "encPubKey", "[B"); - auto enc_sec_key_field = env->GetFieldID(clazz, "encSecKey", "[B"); - auto priority_field = env->GetFieldID(clazz, "priority", "J"); - auto disappearing_timer_field = env->GetFieldID(clazz, "disappearingTimer", "J"); - auto joined_at_field = env->GetFieldID(clazz, "joinedAtSecs", "J"); - auto id = static_cast(env->GetObjectField(info, id_field)); - jstring name = static_cast(env->GetObjectField(info, name_field)); - jobject members_map = env->GetObjectField(info, members_field); - jbyteArray enc_pub_key = static_cast(env->GetObjectField(info, enc_pub_key_field)); - jbyteArray enc_sec_key = static_cast(env->GetObjectField(info, enc_sec_key_field)); - int priority = env->GetLongField(info, priority_field); - long joined_at = env->GetLongField(info, joined_at_field); - - auto id_bytes = util::string_from_jstring(env, id); - auto name_bytes = env->GetStringUTFChars(name, nullptr); - auto enc_pub_key_bytes = util::ustring_from_bytes(env, enc_pub_key); - auto enc_sec_key_bytes = util::ustring_from_bytes(env, enc_sec_key); - - auto info_deserialized = conf->get_or_construct_legacy_group(id_bytes); - - auto current_members = info_deserialized.members(); - for (auto member = current_members.begin(); member != current_members.end(); ++member) { - info_deserialized.erase(member->first); - } - deserialize_members_into(env, members_map, info_deserialized); - info_deserialized.name = name_bytes; - info_deserialized.enc_pubkey = enc_pub_key_bytes; - info_deserialized.enc_seckey = enc_sec_key_bytes; - info_deserialized.priority = priority; - info_deserialized.disappearing_timer = std::chrono::seconds(env->GetLongField(info, disappearing_timer_field)); - info_deserialized.joined_at = joined_at; - env->ReleaseStringUTFChars(name, name_bytes); - return info_deserialized; -} - -inline session::config::community_info deserialize_community_info(JNIEnv *env, jobject info, session::config::UserGroups* conf) { - auto clazz = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$CommunityGroupInfo"); - auto base_info = env->GetFieldID(clazz, "community", "Lnetwork/loki/messenger/libsession_util/util/BaseCommunityInfo;"); - auto priority = env->GetFieldID(clazz, "priority", "J"); - jobject base_community_info = env->GetObjectField(info, base_info); - auto deserialized_base_info = util::deserialize_base_community(env, base_community_info); - int deserialized_priority = env->GetLongField(info, priority); - auto community_info = conf->get_or_construct_community(deserialized_base_info.base_url(), deserialized_base_info.room(), deserialized_base_info.pubkey_hex()); - community_info.priority = deserialized_priority; - return community_info; -} - -inline jobject serialize_members(JNIEnv *env, std::map members_map) { - jclass map_class = env->FindClass("java/util/HashMap"); - jclass boxed_bool = env->FindClass("java/lang/Boolean"); - jmethodID map_constructor = env->GetMethodID(map_class, "", "()V"); - jmethodID insert = env->GetMethodID(map_class, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); - jmethodID new_bool = env->GetMethodID(boxed_bool, "", "(Z)V"); - - jobject new_map = env->NewObject(map_class, map_constructor); - for (auto it = members_map.begin(); it != members_map.end(); it++) { - auto account_id = env->NewStringUTF(it->first.data()); - bool is_admin = it->second; - auto jbool = env->NewObject(boxed_bool, new_bool, is_admin); - env->CallObjectMethod(new_map, insert, account_id, jbool); - } - return new_map; -} - -inline jobject serialize_legacy_group_info(JNIEnv *env, session::config::legacy_group_info info) { - jstring account_id = env->NewStringUTF(info.session_id.data()); - jstring name = env->NewStringUTF(info.name.data()); - jobject members = serialize_members(env, info.members()); - jbyteArray enc_pubkey = util::bytes_from_ustring(env, info.enc_pubkey); - jbyteArray enc_seckey = util::bytes_from_ustring(env, info.enc_seckey); - long long priority = info.priority; - long long joined_at = info.joined_at; - - jclass legacy_group_class = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$LegacyGroupInfo"); - jmethodID constructor = env->GetMethodID(legacy_group_class, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;[B[BJJJ)V"); - jobject serialized = env->NewObject(legacy_group_class, constructor, account_id, name, members, enc_pubkey, enc_seckey, priority, (jlong) info.disappearing_timer.count(), joined_at); - return serialized; -} - -inline jobject serialize_closed_group_info(JNIEnv* env, session::config::group_info info) { - auto session_id = util::serialize_account_id(env, info.id); - jbyteArray admin_bytes = info.secretkey.empty() ? nullptr : util::bytes_from_ustring(env, info.secretkey); - jbyteArray auth_bytes = info.auth_data.empty() ? nullptr : util::bytes_from_ustring(env, info.auth_data); - jstring name = util::jstringFromOptional(env, info.name); - - jclass group_info_class = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$ClosedGroupInfo"); - jmethodID constructor = env->GetMethodID(group_info_class, "","(Lorg/session/libsignal/utilities/AccountId;[B[BJZLjava/lang/String;ZZJ)V"); - jobject return_object = env->NewObject(group_info_class,constructor, - session_id, admin_bytes, auth_bytes, (jlong)info.priority, info.invited, name, - info.kicked(), info.is_destroyed(), info.joined_at); - return return_object; -} - -inline session::config::group_info deserialize_closed_group_info(JNIEnv* env, jobject info_serialized) { - jclass closed_group_class = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$ClosedGroupInfo"); - jfieldID id_field = env->GetFieldID(closed_group_class, "groupAccountId", "Lorg/session/libsignal/utilities/AccountId;"); - jfieldID secret_field = env->GetFieldID(closed_group_class, "adminKey", "[B"); - jfieldID auth_field = env->GetFieldID(closed_group_class, "authData", "[B"); - jfieldID priority_field = env->GetFieldID(closed_group_class, "priority", "J"); - jfieldID invited_field = env->GetFieldID(closed_group_class, "invited", "Z"); - jfieldID name_field = env->GetFieldID(closed_group_class, "name", "Ljava/lang/String;"); - jfieldID destroy_field = env->GetFieldID(closed_group_class, "destroyed", "Z"); - jfieldID kicked_field = env->GetFieldID(closed_group_class, "kicked", "Z"); - jfieldID joined_at_field = env->GetFieldID(closed_group_class, "joinedAtSecs", "J"); - - - jobject id_jobject = env->GetObjectField(info_serialized, id_field); - jbyteArray secret_jBytes = (jbyteArray)env->GetObjectField(info_serialized, secret_field); - jbyteArray auth_jBytes = (jbyteArray)env->GetObjectField(info_serialized, auth_field); - jstring name_jstring = (jstring)env->GetObjectField(info_serialized, name_field); - - auto id_bytes = util::deserialize_account_id(env, id_jobject); - auto secret_bytes = util::ustring_from_bytes(env, secret_jBytes); - auto auth_bytes = util::ustring_from_bytes(env, auth_jBytes); - auto name = util::string_from_jstring(env, name_jstring); - - session::config::group_info group_info(id_bytes); - group_info.auth_data = auth_bytes; - group_info.secretkey = secret_bytes; - group_info.priority = env->GetLongField(info_serialized, priority_field); - group_info.invited = env->GetBooleanField(info_serialized, invited_field); - group_info.name = name; - group_info.joined_at = env->GetLongField(info_serialized, joined_at_field); - - if (env->GetBooleanField(info_serialized, kicked_field)) { - group_info.mark_kicked(); - } - - if (env->GetBooleanField(info_serialized, destroy_field)) { - group_info.mark_destroyed(); - } - - return group_info; -} - -inline jobject serialize_community_info(JNIEnv *env, session::config::community_info info) { - auto priority = (long long)info.priority; - auto serialized_info = util::serialize_base_community(env, info); - auto clazz = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$CommunityGroupInfo"); - jmethodID constructor = env->GetMethodID(clazz, "", "(Lnetwork/loki/messenger/libsession_util/util/BaseCommunityInfo;J)V"); - jobject serialized = env->NewObject(clazz, constructor, serialized_info, priority); - return serialized; -} - -#endif //SESSION_ANDROID_USER_GROUPS_H diff --git a/libsession-util/src/main/cpp/user_profile.cpp b/libsession-util/src/main/cpp/user_profile.cpp deleted file mode 100644 index 47def9b6b57..00000000000 --- a/libsession-util/src/main/cpp/user_profile.cpp +++ /dev/null @@ -1,119 +0,0 @@ -#include "user_profile.h" -#include "util.h" - -extern "C" { -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_UserProfile_setName( - JNIEnv* env, - jobject thiz, - jstring newName) { - std::lock_guard lock{util::util_mutex_}; - auto profile = ptrToProfile(env, thiz); - auto name_chars = env->GetStringUTFChars(newName, nullptr); - profile->set_name(name_chars); - env->ReleaseStringUTFChars(newName, name_chars); -} - -JNIEXPORT jstring JNICALL -Java_network_loki_messenger_libsession_1util_UserProfile_getName(JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto profile = ptrToProfile(env, thiz); - auto name = profile->get_name(); - if (name == std::nullopt) return nullptr; - jstring returnString = env->NewStringUTF(name->data()); - return returnString; -} - -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_UserProfile_getPic(JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto profile = ptrToProfile(env, thiz); - auto pic = profile->get_profile_pic(); - - jobject returnObject = util::serialize_user_pic(env, pic); - - return returnObject; -} - -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_UserProfile_setPic(JNIEnv *env, jobject thiz, - jobject user_pic) { - std::lock_guard lock{util::util_mutex_}; - auto profile = ptrToProfile(env, thiz); - auto pic = util::deserialize_user_pic(env, user_pic); - auto url = env->GetStringUTFChars(pic.first, nullptr); - auto key = util::ustring_from_bytes(env, pic.second); - profile->set_profile_pic(url, key); - env->ReleaseStringUTFChars(pic.first, url); -} - -} -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_UserProfile_setNtsPriority(JNIEnv *env, jobject thiz, - jlong priority) { - std::lock_guard lock{util::util_mutex_}; - auto profile = ptrToProfile(env, thiz); - profile->set_nts_priority(priority); -} -extern "C" -JNIEXPORT jlong JNICALL -Java_network_loki_messenger_libsession_1util_UserProfile_getNtsPriority(JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto profile = ptrToProfile(env, thiz); - return profile->get_nts_priority(); -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_UserProfile_setNtsExpiry(JNIEnv *env, jobject thiz, - jobject expiry_mode) { - std::lock_guard lock{util::util_mutex_}; - auto profile = ptrToProfile(env, thiz); - auto expiry = util::deserialize_expiry(env, expiry_mode); - profile->set_nts_expiry(std::chrono::seconds (expiry.second)); -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_UserProfile_getNtsExpiry(JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto profile = ptrToProfile(env, thiz); - auto nts_expiry = profile->get_nts_expiry(); - if (nts_expiry == std::nullopt) { - auto expiry = util::serialize_expiry(env, session::config::expiration_mode::none, std::chrono::seconds(0)); - return expiry; - } - auto expiry = util::serialize_expiry(env, session::config::expiration_mode::after_send, std::chrono::seconds(*nts_expiry)); - return expiry; -} - -extern "C" -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_UserProfile_getCommunityMessageRequests( - JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto profile = ptrToProfile(env, thiz); - auto blinded_msg_requests = profile->get_blinded_msgreqs(); - if (blinded_msg_requests.has_value()) { - return *blinded_msg_requests; - } - return true; -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_UserProfile_setCommunityMessageRequests( - JNIEnv *env, jobject thiz, jboolean blocks) { - std::lock_guard lock{util::util_mutex_}; - auto profile = ptrToProfile(env, thiz); - profile->set_blinded_msgreqs(std::optional{(bool)blocks}); -} -extern "C" -JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_UserProfile_isBlockCommunityMessageRequestsSet( - JNIEnv *env, jobject thiz) { - std::lock_guard lock{util::util_mutex_}; - auto profile = ptrToProfile(env, thiz); - return profile->get_blinded_msgreqs().has_value(); -} diff --git a/libsession-util/src/main/cpp/user_profile.h b/libsession-util/src/main/cpp/user_profile.h deleted file mode 100644 index cb1b8d973b9..00000000000 --- a/libsession-util/src/main/cpp/user_profile.h +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef SESSION_ANDROID_USER_PROFILE_H -#define SESSION_ANDROID_USER_PROFILE_H - -#include "session/config/user_profile.hpp" -#include -#include - -inline session::config::UserProfile* ptrToProfile(JNIEnv* env, jobject obj) { - jclass configClass = env->FindClass("network/loki/messenger/libsession_util/UserProfile"); - jfieldID pointerField = env->GetFieldID(configClass, "pointer", "J"); - return (session::config::UserProfile*) env->GetLongField(obj, pointerField); -} - -#endif \ No newline at end of file diff --git a/libsession-util/src/main/cpp/util.cpp b/libsession-util/src/main/cpp/util.cpp deleted file mode 100644 index 70979c4ca58..00000000000 --- a/libsession-util/src/main/cpp/util.cpp +++ /dev/null @@ -1,414 +0,0 @@ -#include "util.h" -#include "sodium/randombytes.h" -#include -#include -#include - -#include - -#define LOG_TAG "libsession_util" - -#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__) -#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__) -#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__) -#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) - -namespace util { - - std::mutex util_mutex_ = std::mutex(); - - jbyteArray bytes_from_ustring(JNIEnv* env, session::ustring_view from_str) { - size_t length = from_str.length(); - auto jlength = (jsize)length; - jbyteArray new_array = env->NewByteArray(jlength); - env->SetByteArrayRegion(new_array, 0, jlength, (jbyte*)from_str.data()); - return new_array; - } - - session::ustring ustring_from_bytes(JNIEnv* env, jbyteArray byteArray) { - if (byteArray == nullptr) { - return {}; - } - size_t len = env->GetArrayLength(byteArray); - auto bytes = env->GetByteArrayElements(byteArray, nullptr); - - session::ustring st{reinterpret_cast(bytes), len}; - env->ReleaseByteArrayElements(byteArray, bytes, 0); - return st; - } - - std::string string_from_jstring(JNIEnv* env, jstring string) { - size_t len = env->GetStringUTFLength(string); - auto chars = env->GetStringUTFChars(string, nullptr); - - std::string st(chars, len); - env->ReleaseStringUTFChars(string, chars); - return st; - } - - jobject serialize_user_pic(JNIEnv *env, session::config::profile_pic pic) { - jclass returnObjectClass = env->FindClass("network/loki/messenger/libsession_util/util/UserPic"); - jmethodID constructor = env->GetMethodID(returnObjectClass, "", "(Ljava/lang/String;[B)V"); - jstring url = env->NewStringUTF(pic.url.data()); - jbyteArray byteArray = util::bytes_from_ustring(env, pic.key); - return env->NewObject(returnObjectClass, constructor, url, byteArray); - } - - std::pair deserialize_user_pic(JNIEnv *env, jobject user_pic) { - jclass userPicClass = env->FindClass("network/loki/messenger/libsession_util/util/UserPic"); - jfieldID picField = env->GetFieldID(userPicClass, "url", "Ljava/lang/String;"); - jfieldID keyField = env->GetFieldID(userPicClass, "key", "[B"); - auto pic = (jstring)env->GetObjectField(user_pic, picField); - auto key = (jbyteArray)env->GetObjectField(user_pic, keyField); - return {pic, key}; - } - - jobject serialize_base_community(JNIEnv *env, const session::config::community& community) { - jclass base_community_clazz = env->FindClass("network/loki/messenger/libsession_util/util/BaseCommunityInfo"); - jmethodID base_community_constructor = env->GetMethodID(base_community_clazz, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); - auto base_url = env->NewStringUTF(community.base_url().data()); - auto room = env->NewStringUTF(community.room().data()); - auto pubkey_jstring = env->NewStringUTF(community.pubkey_hex().data()); - jobject ret = env->NewObject(base_community_clazz, base_community_constructor, base_url, room, pubkey_jstring); - return ret; - } - - session::config::community deserialize_base_community(JNIEnv *env, jobject base_community) { - jclass base_community_clazz = env->FindClass("network/loki/messenger/libsession_util/util/BaseCommunityInfo"); - jfieldID base_url_field = env->GetFieldID(base_community_clazz, "baseUrl", "Ljava/lang/String;"); - jfieldID room_field = env->GetFieldID(base_community_clazz, "room", "Ljava/lang/String;"); - jfieldID pubkey_hex_field = env->GetFieldID(base_community_clazz, "pubKeyHex", "Ljava/lang/String;"); - auto base_url = (jstring)env->GetObjectField(base_community,base_url_field); - auto room = (jstring)env->GetObjectField(base_community, room_field); - auto pub_key_hex = (jstring)env->GetObjectField(base_community, pubkey_hex_field); - auto base_url_chars = env->GetStringUTFChars(base_url, nullptr); - auto room_chars = env->GetStringUTFChars(room, nullptr); - auto pub_key_hex_chars = env->GetStringUTFChars(pub_key_hex, nullptr); - - auto community = session::config::community(base_url_chars, room_chars, pub_key_hex_chars); - - env->ReleaseStringUTFChars(base_url, base_url_chars); - env->ReleaseStringUTFChars(room, room_chars); - env->ReleaseStringUTFChars(pub_key_hex, pub_key_hex_chars); - return community; - } - - jobject serialize_expiry(JNIEnv *env, const session::config::expiration_mode& mode, const std::chrono::seconds& time_seconds) { - jclass none = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode$NONE"); - jfieldID none_instance = env->GetStaticFieldID(none, "INSTANCE", "Lnetwork/loki/messenger/libsession_util/util/ExpiryMode$NONE;"); - jclass after_send = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode$AfterSend"); - jmethodID send_init = env->GetMethodID(after_send, "", "(J)V"); - jclass after_read = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode$AfterRead"); - jmethodID read_init = env->GetMethodID(after_read, "", "(J)V"); - - if (mode == session::config::expiration_mode::none) { - return env->GetStaticObjectField(none, none_instance); - } else if (mode == session::config::expiration_mode::after_send) { - return env->NewObject(after_send, send_init, time_seconds.count()); - } else if (mode == session::config::expiration_mode::after_read) { - return env->NewObject(after_read, read_init, time_seconds.count()); - } - return nullptr; - } - - std::pair deserialize_expiry(JNIEnv *env, jobject expiry_mode) { - jclass parent = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode"); - jclass after_read = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode$AfterRead"); - jclass after_send = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode$AfterSend"); - jfieldID duration_seconds = env->GetFieldID(parent, "expirySeconds", "J"); - - jclass object_class = env->GetObjectClass(expiry_mode); - - if (env->IsSameObject(object_class, after_read)) { - return std::pair(session::config::expiration_mode::after_read, env->GetLongField(expiry_mode, duration_seconds)); - } else if (env->IsSameObject(object_class, after_send)) { - return std::pair(session::config::expiration_mode::after_send, env->GetLongField(expiry_mode, duration_seconds)); - } - return std::pair(session::config::expiration_mode::none, 0); - } - - jobject build_string_stack(JNIEnv* env, std::vector to_add) { - jclass stack_class = env->FindClass("java/util/Stack"); - jmethodID constructor = env->GetMethodID(stack_class,"", "()V"); - jmethodID add = env->GetMethodID(stack_class, "push", "(Ljava/lang/Object;)Ljava/lang/Object;"); - jobject our_stack = env->NewObject(stack_class, constructor); - for (std::basic_string_view string: to_add) { - env->CallObjectMethod(our_stack, add, env->NewStringUTF(string.data())); - } - return our_stack; - } - - jobject serialize_group_member(JNIEnv* env, const session::config::groups::member& member) { - jclass group_member_class = env->FindClass("network/loki/messenger/libsession_util/util/GroupMember"); - jmethodID constructor = env->GetMethodID(group_member_class, "", "(J)V"); - return env->NewObject(group_member_class, - constructor, - reinterpret_cast(new session::config::groups::member(member)) - ); - } - - jobject deserialize_swarm_auth(JNIEnv *env, session::config::groups::Keys::swarm_auth auth) { - jclass swarm_auth_class = env->FindClass("network/loki/messenger/libsession_util/GroupKeysConfig$SwarmAuth"); - jmethodID constructor = env->GetMethodID(swarm_auth_class, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); - jstring sub_account = env->NewStringUTF(auth.subaccount.data()); - jstring sub_account_sig = env->NewStringUTF(auth.subaccount_sig.data()); - jstring signature = env->NewStringUTF(auth.signature.data()); - - return env->NewObject(swarm_auth_class, constructor, sub_account, sub_account_sig, signature); - } - - jobject jlongFromOptional(JNIEnv* env, std::optional optional) { - if (!optional) { - return nullptr; - } - jclass longClass = env->FindClass("java/lang/Long"); - jmethodID constructor = env->GetMethodID(longClass, "", "(J)V"); - jobject returned = env->NewObject(longClass, constructor, (jlong)*optional); - return returned; - } - - jstring jstringFromOptional(JNIEnv* env, std::optional optional) { - if (!optional) { - return nullptr; - } - return env->NewStringUTF(optional->data()); - } - - jobject serialize_account_id(JNIEnv* env, std::string_view session_id) { - if (session_id.size() != 66) return nullptr; - - jclass id_class = env->FindClass("org/session/libsignal/utilities/AccountId"); - jmethodID session_id_constructor = env->GetMethodID(id_class, "", "(Ljava/lang/String;)V"); - - jstring session_id_string = env->NewStringUTF(session_id.data()); - - return env->NewObject(id_class, session_id_constructor, session_id_string); - } - - std::string deserialize_account_id(JNIEnv* env, jobject account_id) { - jclass session_id_class = env->FindClass("org/session/libsignal/utilities/AccountId"); - jmethodID get_string = env->GetMethodID(session_id_class, "getHexString", "()Ljava/lang/String;"); - auto hex_jstring = (jstring)env->CallObjectMethod(account_id, get_string); - auto hex_bytes = env->GetStringUTFChars(hex_jstring, nullptr); - std::string hex_string{hex_bytes}; - env->ReleaseStringUTFChars(hex_jstring, hex_bytes); - return hex_string; - } - -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_util_Sodium_ed25519KeyPair(JNIEnv *env, jobject thiz, jbyteArray seed) { - std::array ed_pk; // NOLINT(cppcoreguidelines-pro-type-member-init) - std::array ed_sk; // NOLINT(cppcoreguidelines-pro-type-member-init) - auto seed_bytes = util::ustring_from_bytes(env, seed); - crypto_sign_ed25519_seed_keypair(ed_pk.data(), ed_sk.data(), seed_bytes.data()); - - jclass kp_class = env->FindClass("network/loki/messenger/libsession_util/util/KeyPair"); - jmethodID kp_constructor = env->GetMethodID(kp_class, "", "([B[B)V"); - - jbyteArray pk_jarray = util::bytes_from_ustring(env, session::ustring_view {ed_pk.data(), ed_pk.size()}); - jbyteArray sk_jarray = util::bytes_from_ustring(env, session::ustring_view {ed_sk.data(), ed_sk.size()}); - - jobject return_obj = env->NewObject(kp_class, kp_constructor, pk_jarray, sk_jarray); - return return_obj; -} - -extern "C" -JNIEXPORT jbyteArray JNICALL -Java_network_loki_messenger_libsession_1util_util_Sodium_ed25519PkToCurve25519(JNIEnv *env, - jobject thiz, - jbyteArray pk) { - auto ed_pk = util::ustring_from_bytes(env, pk); - std::array curve_pk; // NOLINT(cppcoreguidelines-pro-type-member-init) - int success = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); - if (success != 0) { - jclass exception = env->FindClass("java/lang/Exception"); - env->ThrowNew(exception, "Invalid crypto_sign_ed25519_pk_to_curve25519 operation"); - return nullptr; - } - jbyteArray curve_pk_jarray = util::bytes_from_ustring(env, session::ustring_view {curve_pk.data(), curve_pk.size()}); - return curve_pk_jarray; -} - -extern "C" -JNIEXPORT jbyteArray JNICALL -Java_network_loki_messenger_libsession_1util_util_Sodium_encryptForMultipleSimple( - JNIEnv *env, jobject thiz, jobjectArray messages, jobjectArray recipients, - jbyteArray ed25519_secret_key, jstring domain) { - // messages and recipients have to be the same size - uint size = env->GetArrayLength(messages); - if (env->GetArrayLength(recipients) != size) { - env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"), "Messages and recipients must be the same size"); - return nullptr; - } - std::vector message_vec{}; - std::vector recipient_vec{}; - for (int i = 0; i < size; i++) { - jbyteArray message_j = static_cast(env->GetObjectArrayElement(messages, i)); - jbyteArray recipient_j = static_cast(env->GetObjectArrayElement(recipients, i)); - session::ustring message = util::ustring_from_bytes(env, message_j); - session::ustring recipient = util::ustring_from_bytes(env, recipient_j); - - message_vec.emplace_back(session::ustring{message}); - recipient_vec.emplace_back(session::ustring{recipient}); - } - - std::vector message_sv_vec{}; - std::vector recipient_sv_vec{}; - for (int i = 0; i < size; i++) { - message_sv_vec.emplace_back(session::to_unsigned_sv(message_vec[i])); - recipient_sv_vec.emplace_back(session::to_unsigned_sv(recipient_vec[i])); - } - - auto sk = util::ustring_from_bytes(env, ed25519_secret_key); - std::array random_nonce; - randombytes_buf(random_nonce.data(), random_nonce.size()); - - auto domain_string = env->GetStringUTFChars(domain, nullptr); - - auto result = session::encrypt_for_multiple_simple( - message_sv_vec, - recipient_sv_vec, - sk, - domain_string, - session::ustring_view {random_nonce.data(), 24} - ); - - env->ReleaseStringUTFChars(domain, domain_string); - auto encoded = util::bytes_from_ustring(env, result); - return encoded; -} - -extern "C" -JNIEXPORT jbyteArray JNICALL -Java_network_loki_messenger_libsession_1util_util_Sodium_decryptForMultipleSimple(JNIEnv *env, - jobject thiz, - jbyteArray encoded, - jbyteArray secret_key, - jbyteArray sender_pub_key, - jstring domain) { - auto sk_ustring = util::ustring_from_bytes(env, secret_key); - auto encoded_ustring = util::ustring_from_bytes(env, encoded); - auto pub_ustring = util::ustring_from_bytes(env, sender_pub_key); - auto domain_bytes = env->GetStringUTFChars(domain, nullptr); - auto result = session::decrypt_for_multiple_simple( - encoded_ustring, - sk_ustring, - pub_ustring, - domain_bytes - ); - env->ReleaseStringUTFChars(domain,domain_bytes); - if (result) { - return util::bytes_from_ustring(env, *result); - } else { - LOGD("no result from decrypt"); - } - return nullptr; -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_util_BaseCommunityInfo_00024Companion_parseFullUrl( - JNIEnv *env, jobject thiz, jstring full_url) { - auto bytes = env->GetStringUTFChars(full_url, nullptr); - auto [base, room, pk] = session::config::community::parse_full_url(bytes); - env->ReleaseStringUTFChars(full_url, bytes); - - jclass clazz = env->FindClass("kotlin/Triple"); - jmethodID constructor = env->GetMethodID(clazz, "", "(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V"); - - auto base_j = env->NewStringUTF(base.data()); - auto room_j = env->NewStringUTF(room.data()); - auto pk_jbytes = util::bytes_from_ustring(env, pk); - - jobject triple = env->NewObject(clazz, constructor, base_j, room_j, pk_jbytes); - return triple; -} -extern "C" -JNIEXPORT jstring JNICALL -Java_network_loki_messenger_libsession_1util_util_BaseCommunityInfo_fullUrl(JNIEnv *env, - jobject thiz) { - auto deserialized = util::deserialize_base_community(env, thiz); - auto full_url = deserialized.full_url(); - return env->NewStringUTF(full_url.data()); -} - -extern "C" -JNIEXPORT jint JNICALL -Java_org_session_libsignal_utilities_Namespace_DEFAULT(JNIEnv *env, jobject thiz) { - return 0; -} - -extern "C" -JNIEXPORT jint JNICALL -Java_org_session_libsignal_utilities_Namespace_USER_1PROFILE(JNIEnv *env, jobject thiz) { - return (int) session::config::Namespace::UserProfile; -} - -extern "C" -JNIEXPORT jint JNICALL -Java_org_session_libsignal_utilities_Namespace_CONTACTS(JNIEnv *env, jobject thiz) { - return (int) session::config::Namespace::Contacts; -} - -extern "C" -JNIEXPORT jint JNICALL -Java_org_session_libsignal_utilities_Namespace_CONVO_1INFO_1VOLATILE(JNIEnv *env, jobject thiz) { - return (int) session::config::Namespace::ConvoInfoVolatile; -} - -extern "C" -JNIEXPORT jint JNICALL -Java_org_session_libsignal_utilities_Namespace_USER_1GROUPS(JNIEnv *env, jobject thiz) { - return (int) session::config::Namespace::UserGroups; -} - -extern "C" -JNIEXPORT jint JNICALL -Java_org_session_libsignal_utilities_Namespace_GROUP_1INFO(JNIEnv *env, jobject thiz) { - return (int) session::config::Namespace::GroupInfo; -} - -extern "C" -JNIEXPORT jint JNICALL -Java_org_session_libsignal_utilities_Namespace_GROUP_1MEMBERS(JNIEnv *env, jobject thiz) { - return (int) session::config::Namespace::GroupMembers; -} - -extern "C" -JNIEXPORT jint JNICALL -Java_org_session_libsignal_utilities_Namespace_GROUP_1KEYS(JNIEnv *env, jobject thiz) { - return (int) session::config::Namespace::GroupKeys; -} - -extern "C" -JNIEXPORT jint JNICALL -Java_org_session_libsignal_utilities_Namespace_GROUP_1MESSAGES(JNIEnv *env, jobject thiz) { - return (int) session::config::Namespace::GroupMessages; -} - -extern "C" -JNIEXPORT jint JNICALL -Java_org_session_libsignal_utilities_Namespace_REVOKED_1GROUP_1MESSAGES(JNIEnv *env, jobject thiz) { - return -11; // we don't have revoked namespace in user configs -} - -extern "C" -JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_Config_free(JNIEnv *env, jobject thiz) { - jclass baseClass = env->FindClass("network/loki/messenger/libsession_util/Config"); - jfieldID pointerField = env->GetFieldID(baseClass, "pointer", "J"); - jclass sig = env->FindClass("network/loki/messenger/libsession_util/ConfigSig"); - jclass base = env->FindClass("network/loki/messenger/libsession_util/ConfigBase"); - jclass ours = env->GetObjectClass(thiz); - if (env->IsSameObject(sig, ours)) { - // config sig object - auto config = (session::config::ConfigSig*) env->GetLongField(thiz, pointerField); - delete config; - } else if (env->IsSameObject(base, ours)) { - auto config = (session::config::ConfigBase*) env->GetLongField(thiz, pointerField); - delete config; - } -} \ No newline at end of file diff --git a/libsession-util/src/main/cpp/util.h b/libsession-util/src/main/cpp/util.h deleted file mode 100644 index 234ceb00174..00000000000 --- a/libsession-util/src/main/cpp/util.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef SESSION_ANDROID_UTIL_H -#define SESSION_ANDROID_UTIL_H - -#include -#include -#include -#include "session/types.hpp" -#include "session/config/groups/info.hpp" -#include "session/config/groups/keys.hpp" -#include "session/config/groups/members.hpp" -#include "session/config/profile_pic.hpp" -#include "session/config/user_groups.hpp" -#include "session/config/expiring.hpp" - -namespace util { - extern std::mutex util_mutex_; - jbyteArray bytes_from_ustring(JNIEnv* env, session::ustring_view from_str); - session::ustring ustring_from_bytes(JNIEnv* env, jbyteArray byteArray); - std::string string_from_jstring(JNIEnv* env, jstring string); - jobject serialize_user_pic(JNIEnv *env, session::config::profile_pic pic); - std::pair deserialize_user_pic(JNIEnv *env, jobject user_pic); - jobject serialize_base_community(JNIEnv *env, const session::config::community& base_community); - session::config::community deserialize_base_community(JNIEnv *env, jobject base_community); - jobject serialize_expiry(JNIEnv *env, const session::config::expiration_mode& mode, const std::chrono::seconds& time_seconds); - std::pair deserialize_expiry(JNIEnv *env, jobject expiry_mode); - jobject serialize_group_member(JNIEnv* env, const session::config::groups::member& member); - jobject jlongFromOptional(JNIEnv* env, std::optional optional); - jstring jstringFromOptional(JNIEnv* env, std::optional optional); - jobject serialize_account_id(JNIEnv* env, std::string_view session_id); - std::string deserialize_account_id(JNIEnv* env, jobject account_id); - jobject build_string_stack(JNIEnv* env, std::vector to_add); - jobject deserialize_swarm_auth(JNIEnv *env, session::config::groups::Keys::swarm_auth auth);} - -#endif \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt deleted file mode 100644 index fb2404ca206..00000000000 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt +++ /dev/null @@ -1,563 +0,0 @@ -package network.loki.messenger.libsession_util - -import network.loki.messenger.libsession_util.util.BaseCommunityInfo -import network.loki.messenger.libsession_util.util.ConfigPush -import network.loki.messenger.libsession_util.util.Contact -import network.loki.messenger.libsession_util.util.Conversation -import network.loki.messenger.libsession_util.util.ExpiryMode -import network.loki.messenger.libsession_util.util.GroupInfo -import network.loki.messenger.libsession_util.util.GroupMember -import network.loki.messenger.libsession_util.util.UserPic -import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Kind -import org.session.libsignal.utilities.AccountId -import org.session.libsignal.utilities.IdPrefix -import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.Namespace -import java.io.Closeable -import java.util.Stack - -sealed class Config(initialPointer: Long): Closeable { - var pointer = initialPointer - private set - - init { - check(pointer != 0L) { "Pointer is null" } - } - - abstract fun namespace(): Int - - private external fun free() - - final override fun close() { - if (pointer != 0L) { - free() - pointer = 0L - } - } -} - -interface ReadableConfig { - fun namespace(): Int - fun needsPush(): Boolean - fun needsDump(): Boolean - fun currentHashes(): List -} - -interface MutableConfig : ReadableConfig { - fun push(): ConfigPush - fun dump(): ByteArray - fun encryptionDomain(): String - fun confirmPushed(seqNo: Long, newHash: String) - fun dirty(): Boolean -} - -sealed class ConfigBase(pointer: Long): Config(pointer), MutableConfig { - companion object { - init { - System.loadLibrary("session_util") - } - external fun kindFor(configNamespace: Int): Class - - fun ConfigBase.protoKindFor(): Kind = when (this) { - is UserProfile -> Kind.USER_PROFILE - is Contacts -> Kind.CONTACTS - is ConversationVolatileConfig -> Kind.CONVO_INFO_VOLATILE - is UserGroupsConfig -> Kind.GROUPS - is GroupInfoConfig -> Kind.CLOSED_GROUP_INFO - is GroupMembersConfig -> Kind.CLOSED_GROUP_MEMBERS - } - - const val PRIORITY_HIDDEN = -1L - const val PRIORITY_VISIBLE = 0L - const val PRIORITY_PINNED = 1L - - } - - external override fun dirty(): Boolean - external override fun needsPush(): Boolean - external override fun needsDump(): Boolean - external override fun push(): ConfigPush - external override fun dump(): ByteArray - external override fun encryptionDomain(): String - external override fun confirmPushed(seqNo: Long, newHash: String) - external fun merge(toMerge: Array>): Stack - external override fun currentHashes(): List -} - - -interface ReadableContacts: ReadableConfig { - fun get(accountId: String): Contact? - fun all(): List -} - -interface MutableContacts : ReadableContacts, MutableConfig { - fun getOrConstruct(accountId: String): Contact - fun set(contact: Contact) - fun erase(accountId: String): Boolean - - /** - * Similar to [updateIfExists], but will create the underlying contact if it doesn't exist before passing to [updateFunction] - */ - fun upsertContact(accountId: String, updateFunction: Contact.() -> Unit = {}) { - when { - accountId.startsWith(IdPrefix.BLINDED.value) -> Log.w("Loki", "Trying to create a contact with a blinded ID prefix") - accountId.startsWith(IdPrefix.UN_BLINDED.value) -> Log.w("Loki", "Trying to create a contact with an un-blinded ID prefix") - accountId.startsWith(IdPrefix.BLINDEDV2.value) -> Log.w("Loki", "Trying to create a contact with a blindedv2 ID prefix") - else -> getOrConstruct(accountId).let { - updateFunction(it) - set(it) - } - } - } -} - -class Contacts private constructor(pointer: Long) : ConfigBase(pointer), MutableContacts { - constructor(ed25519SecretKey: ByteArray, initialDump: ByteArray? = null) : this( - createConfigObject( - "Contacts", - ed25519SecretKey, - initialDump - ) - ) - - override fun namespace() = Namespace.CONTACTS() - - external override fun get(accountId: String): Contact? - external override fun getOrConstruct(accountId: String): Contact - external override fun all(): List - external override fun set(contact: Contact) - external override fun erase(accountId: String): Boolean -} - -interface ReadableUserProfile: ReadableConfig { - fun getName(): String? - fun getPic(): UserPic - fun getNtsPriority(): Long - fun getNtsExpiry(): ExpiryMode - fun getCommunityMessageRequests(): Boolean - fun isBlockCommunityMessageRequestsSet(): Boolean -} - -interface MutableUserProfile : ReadableUserProfile, MutableConfig { - fun setName(newName: String) - fun setPic(userPic: UserPic) - fun setNtsPriority(priority: Long) - fun setNtsExpiry(expiryMode: ExpiryMode) - fun setCommunityMessageRequests(blocks: Boolean) -} - -class UserProfile private constructor(pointer: Long) : ConfigBase(pointer), MutableUserProfile { - constructor(ed25519SecretKey: ByteArray, initialDump: ByteArray? = null) : this( - createConfigObject( - "UserProfile", - ed25519SecretKey, - initialDump - ) - ) - - override fun namespace() = Namespace.USER_PROFILE() - - external override fun setName(newName: String) - external override fun getName(): String? - external override fun getPic(): UserPic - external override fun setPic(userPic: UserPic) - external override fun setNtsPriority(priority: Long) - external override fun getNtsPriority(): Long - external override fun setNtsExpiry(expiryMode: ExpiryMode) - external override fun getNtsExpiry(): ExpiryMode - external override fun getCommunityMessageRequests(): Boolean - external override fun setCommunityMessageRequests(blocks: Boolean) - external override fun isBlockCommunityMessageRequestsSet(): Boolean -} - -interface ReadableConversationVolatileConfig: ReadableConfig { - fun getOneToOne(pubKeyHex: String): Conversation.OneToOne? - fun getCommunity(baseUrl: String, room: String): Conversation.Community? - fun getLegacyClosedGroup(groupId: String): Conversation.LegacyGroup? - fun getClosedGroup(sessionId: String): Conversation.ClosedGroup? - fun sizeOneToOnes(): Int - fun sizeCommunities(): Int - fun sizeLegacyClosedGroups(): Int - fun size(): Int - - fun empty(): Boolean - - fun allOneToOnes(): List - fun allCommunities(): List - fun allLegacyClosedGroups(): List - fun allClosedGroups(): List - fun all(): List -} - -interface MutableConversationVolatileConfig : ReadableConversationVolatileConfig, MutableConfig { - fun getOrConstructOneToOne(pubKeyHex: String): Conversation.OneToOne - fun eraseOneToOne(pubKeyHex: String): Boolean - - fun getOrConstructCommunity(baseUrl: String, room: String, pubKeyHex: String): Conversation.Community - fun getOrConstructCommunity(baseUrl: String, room: String, pubKey: ByteArray): Conversation.Community - fun eraseCommunity(community: Conversation.Community): Boolean - fun eraseCommunity(baseUrl: String, room: String): Boolean - - fun getOrConstructLegacyGroup(groupId: String): Conversation.LegacyGroup - fun eraseLegacyClosedGroup(groupId: String): Boolean - - fun getOrConstructClosedGroup(sessionId: String): Conversation.ClosedGroup - fun eraseClosedGroup(sessionId: String): Boolean - - fun erase(conversation: Conversation): Boolean - fun set(toStore: Conversation) - - fun eraseAll(predicate: (Conversation) -> Boolean): Int -} - - -class ConversationVolatileConfig private constructor(pointer: Long): ConfigBase(pointer), MutableConversationVolatileConfig { - constructor(ed25519SecretKey: ByteArray, initialDump: ByteArray? = null) : this( - createConfigObject( - "ConvoInfoVolatile", - ed25519SecretKey, - initialDump - ) - ) - - override fun namespace() = Namespace.CONVO_INFO_VOLATILE() - - external override fun getOneToOne(pubKeyHex: String): Conversation.OneToOne? - external override fun getOrConstructOneToOne(pubKeyHex: String): Conversation.OneToOne - external override fun eraseOneToOne(pubKeyHex: String): Boolean - - external override fun getCommunity(baseUrl: String, room: String): Conversation.Community? - external override fun getOrConstructCommunity(baseUrl: String, room: String, pubKeyHex: String): Conversation.Community - external override fun getOrConstructCommunity(baseUrl: String, room: String, pubKey: ByteArray): Conversation.Community - external override fun eraseCommunity(community: Conversation.Community): Boolean - external override fun eraseCommunity(baseUrl: String, room: String): Boolean - - external override fun getLegacyClosedGroup(groupId: String): Conversation.LegacyGroup? - external override fun getOrConstructLegacyGroup(groupId: String): Conversation.LegacyGroup - external override fun eraseLegacyClosedGroup(groupId: String): Boolean - - external override fun getClosedGroup(sessionId: String): Conversation.ClosedGroup? - external override fun getOrConstructClosedGroup(sessionId: String): Conversation.ClosedGroup - external override fun eraseClosedGroup(sessionId: String): Boolean - - external override fun erase(conversation: Conversation): Boolean - external override fun set(toStore: Conversation) - - /** - * Erase all conversations that do not satisfy the `predicate`, similar to [MutableList.removeAll] - */ - external override fun eraseAll(predicate: (Conversation) -> Boolean): Int - - external override fun sizeOneToOnes(): Int - external override fun sizeCommunities(): Int - external override fun sizeLegacyClosedGroups(): Int - external override fun size(): Int - - external override fun empty(): Boolean - - external override fun allOneToOnes(): List - external override fun allCommunities(): List - external override fun allLegacyClosedGroups(): List - external override fun allClosedGroups(): List - external override fun all(): List -} - -interface ReadableUserGroupsConfig : ReadableConfig { - fun getCommunityInfo(baseUrl: String, room: String): GroupInfo.CommunityGroupInfo? - fun getLegacyGroupInfo(accountId: String): GroupInfo.LegacyGroupInfo? - fun getClosedGroup(accountId: String): GroupInfo.ClosedGroupInfo? - fun sizeCommunityInfo(): Long - fun sizeLegacyGroupInfo(): Long - fun sizeClosedGroup(): Long - fun size(): Long - fun all(): List - fun allCommunityInfo(): List - fun allLegacyGroupInfo(): List - fun allClosedGroupInfo(): List - fun createGroup(): GroupInfo.ClosedGroupInfo -} - -interface MutableUserGroupsConfig : ReadableUserGroupsConfig, MutableConfig { - fun getOrConstructCommunityInfo(baseUrl: String, room: String, pubKeyHex: String): GroupInfo.CommunityGroupInfo - fun getOrConstructLegacyGroupInfo(accountId: String): GroupInfo.LegacyGroupInfo - fun getOrConstructClosedGroup(accountId: String): GroupInfo.ClosedGroupInfo - fun set(groupInfo: GroupInfo) - fun erase(groupInfo: GroupInfo) - fun eraseCommunity(baseCommunityInfo: BaseCommunityInfo): Boolean - fun eraseCommunity(server: String, room: String): Boolean - fun eraseLegacyGroup(accountId: String): Boolean - fun eraseClosedGroup(accountId: String): Boolean -} - -class UserGroupsConfig private constructor(pointer: Long): ConfigBase(pointer), MutableUserGroupsConfig { - constructor(ed25519SecretKey: ByteArray, initialDump: ByteArray? = null) : this( - createConfigObject( - "UserGroups", - ed25519SecretKey, - initialDump - ) - ) - - override fun namespace() = Namespace.USER_GROUPS() - - external override fun getCommunityInfo(baseUrl: String, room: String): GroupInfo.CommunityGroupInfo? - external override fun getLegacyGroupInfo(accountId: String): GroupInfo.LegacyGroupInfo? - external override fun getClosedGroup(accountId: String): GroupInfo.ClosedGroupInfo? - external override fun getOrConstructCommunityInfo(baseUrl: String, room: String, pubKeyHex: String): GroupInfo.CommunityGroupInfo - external override fun getOrConstructLegacyGroupInfo(accountId: String): GroupInfo.LegacyGroupInfo - external override fun getOrConstructClosedGroup(accountId: String): GroupInfo.ClosedGroupInfo - external override fun set(groupInfo: GroupInfo) - external override fun erase(groupInfo: GroupInfo) - external override fun eraseCommunity(baseCommunityInfo: BaseCommunityInfo): Boolean - external override fun eraseCommunity(server: String, room: String): Boolean - external override fun eraseLegacyGroup(accountId: String): Boolean - external override fun eraseClosedGroup(accountId: String): Boolean - external override fun sizeCommunityInfo(): Long - external override fun sizeLegacyGroupInfo(): Long - external override fun sizeClosedGroup(): Long - external override fun size(): Long - external override fun all(): List - external override fun allCommunityInfo(): List - external override fun allLegacyGroupInfo(): List - external override fun allClosedGroupInfo(): List - external override fun createGroup(): GroupInfo.ClosedGroupInfo -} - -interface ReadableGroupInfoConfig: ReadableConfig { - fun id(): AccountId - fun getDeleteAttachmentsBefore(): Long? - fun getDeleteBefore(): Long? - fun getExpiryTimer(): Long - fun getName(): String? - fun getCreated(): Long? - fun getProfilePic(): UserPic - fun isDestroyed(): Boolean - fun getDescription(): String - fun storageNamespace(): Long -} - -interface MutableGroupInfoConfig : ReadableGroupInfoConfig, MutableConfig { - fun setCreated(createdAt: Long) - fun setDeleteAttachmentsBefore(deleteBefore: Long) - fun setDeleteBefore(deleteBefore: Long) - fun setExpiryTimer(expireSeconds: Long) - fun setName(newName: String) - fun setDescription(newDescription: String) - fun setProfilePic(newProfilePic: UserPic) - fun destroyGroup() -} - -class GroupInfoConfig private constructor(pointer: Long): ConfigBase(pointer), MutableGroupInfoConfig { - constructor(groupPubKey: ByteArray, groupAdminKey: ByteArray?, initialDump: ByteArray?) - : this(newInstance(groupPubKey, groupAdminKey, initialDump)) - - companion object { - private external fun newInstance( - pubKey: ByteArray, - secretKey: ByteArray?, - initialDump: ByteArray? - ): Long - } - - override fun namespace() = Namespace.GROUP_INFO() - - external override fun id(): AccountId - external override fun destroyGroup() - external override fun getCreated(): Long? - external override fun getDeleteAttachmentsBefore(): Long? - external override fun getDeleteBefore(): Long? - external override fun getExpiryTimer(): Long - external override fun getName(): String? - external override fun getProfilePic(): UserPic - external override fun isDestroyed(): Boolean - external override fun setCreated(createdAt: Long) - external override fun setDeleteAttachmentsBefore(deleteBefore: Long) - external override fun setDeleteBefore(deleteBefore: Long) - external override fun setExpiryTimer(expireSeconds: Long) - external override fun setName(newName: String) - external override fun getDescription(): String - external override fun setDescription(newDescription: String) - external override fun setProfilePic(newProfilePic: UserPic) - external override fun storageNamespace(): Long -} - -interface ReadableGroupMembersConfig: ReadableConfig { - fun all(): List - - /** - * Returns the [GroupMember] for the given [pubKeyHex] or null if it doesn't exist. - * Note: exception will be thrown if the [pubKeyHex] is invalid. You can opt to use [getOrNull] instead - */ - fun get(pubKeyHex: String): GroupMember? - fun status(groupMember: GroupMember): GroupMember.Status -} - -fun ReadableGroupMembersConfig.allWithStatus(): Sequence> { - return all().asSequence().map { it to status(it) } -} - -/** - * Returns the [GroupMember] for the given [pubKeyHex] or null if it doesn't exist or is invalid - */ -fun ReadableGroupMembersConfig.getOrNull(pubKeyHex: String): GroupMember? { - return runCatching { - get(pubKeyHex) - }.getOrNull() -} - -interface MutableGroupMembersConfig : ReadableGroupMembersConfig, MutableConfig { - fun getOrConstruct(pubKeyHex: String): GroupMember - fun set(groupMember: GroupMember) - fun erase(pubKeyHex: String): Boolean - - fun setPendingSend(pubKeyHex: String, pending: Boolean) -} - -class GroupMembersConfig private constructor(pointer: Long): ConfigBase(pointer), MutableGroupMembersConfig { - companion object { - private external fun newInstance( - pubKey: ByteArray, - secretKey: ByteArray?, - initialDump: ByteArray? - ): Long - } - - constructor(groupPubKey: ByteArray, groupAdminKey: ByteArray?, initialDump: ByteArray?) - : this(newInstance(groupPubKey, groupAdminKey, initialDump)) - - override fun namespace() = Namespace.GROUP_MEMBERS() - - external override fun all(): Stack - external override fun erase(pubKeyHex: String): Boolean - external override fun get(pubKeyHex: String): GroupMember? - external override fun getOrConstruct(pubKeyHex: String): GroupMember - external override fun set(groupMember: GroupMember) - external override fun setPendingSend(pubKeyHex: String, pending: Boolean) - - private external fun statusInt(groupMember: GroupMember): Int - override fun status(groupMember: GroupMember): GroupMember.Status { - val statusInt = statusInt(groupMember) - return GroupMember.Status.entries.first { it.nativeValue == statusInt } - } -} - -sealed class ConfigSig(pointer: Long) : Config(pointer) - -interface ReadableGroupKeysConfig { - fun groupKeys(): Stack - fun needsDump(): Boolean - fun dump(): ByteArray - fun needsRekey(): Boolean - fun pendingKey(): ByteArray? - fun supplementFor(userSessionIds: List): ByteArray - fun pendingConfig(): ByteArray? - fun currentHashes(): List - fun encrypt(plaintext: ByteArray): ByteArray - fun decrypt(ciphertext: ByteArray): Pair? - fun keys(): Stack - fun subAccountSign(message: ByteArray, signingValue: ByteArray): GroupKeysConfig.SwarmAuth - fun getSubAccountToken(sessionId: AccountId, canWrite: Boolean = true, canDelete: Boolean = false): ByteArray - fun currentGeneration(): Int - fun size(): Int -} - -interface MutableGroupKeysConfig : ReadableGroupKeysConfig { - fun makeSubAccount(sessionId: AccountId, canWrite: Boolean = true, canDelete: Boolean = false): ByteArray - fun loadKey(message: ByteArray, hash: String, timestampMs: Long): Boolean - fun loadAdminKey(adminKey: ByteArray) -} - -class GroupKeysConfig private constructor( - pointer: Long, - private val info: GroupInfoConfig, - private val members: GroupMembersConfig -): ConfigSig(pointer), MutableGroupKeysConfig { - companion object { - private external fun newInstance( - userSecretKey: ByteArray, - groupPublicKey: ByteArray, - groupSecretKey: ByteArray? = null, - initialDump: ByteArray?, - infoPtr: Long, - members: Long - ): Long - } - - constructor( - userSecretKey: ByteArray, - groupPublicKey: ByteArray, - groupAdminKey: ByteArray?, - initialDump: ByteArray?, - info: GroupInfoConfig, - members: GroupMembersConfig - ) : this( - newInstance( - userSecretKey, - groupPublicKey, - groupAdminKey, - initialDump, - info.pointer, - members.pointer - ), - info, - members - ) - - override fun namespace() = Namespace.GROUP_KEYS() - - external override fun groupKeys(): Stack - external override fun needsDump(): Boolean - external override fun dump(): ByteArray - external fun loadKey(message: ByteArray, - hash: String, - timestampMs: Long, - infoPtr: Long, - membersPtr: Long): Boolean - - override fun loadKey(message: ByteArray, hash: String, timestampMs: Long): Boolean { - return loadKey(message, hash, timestampMs, info.pointer, members.pointer) - } - - override fun loadAdminKey(adminKey: ByteArray) { - loadAdminKey(adminKey, info.pointer, members.pointer) - } - - private external fun loadAdminKey(adminKey: ByteArray, infoPtr: Long, membersPtr: Long) - - external override fun needsRekey(): Boolean - external override fun pendingKey(): ByteArray? - private external fun supplementFor(userSessionIds: Array): ByteArray - override fun supplementFor(userSessionIds: List): ByteArray { - return supplementFor(userSessionIds.toTypedArray()) - } - - external override fun pendingConfig(): ByteArray? - external override fun currentHashes(): List - external fun rekey(infoPtr: Long, membersPtr: Long): ByteArray - - external override fun encrypt(plaintext: ByteArray): ByteArray - external override fun decrypt(ciphertext: ByteArray): Pair? - - external override fun keys(): Stack - - external override fun makeSubAccount(sessionId: AccountId, canWrite: Boolean, canDelete: Boolean): ByteArray - external override fun getSubAccountToken(sessionId: AccountId, canWrite: Boolean, canDelete: Boolean): ByteArray - - external override fun subAccountSign(message: ByteArray, signingValue: ByteArray): SwarmAuth - - external override fun currentGeneration(): Int - external fun admin(): Boolean - external override fun size(): Int - - data class SwarmAuth( - val subAccount: String, - val subAccountSig: String, - val signature: String - ) -} - -private external fun createConfigObject( - configName: String, - ed25519SecretKey: ByteArray, - initialDump: ByteArray? -): Long \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BaseCommunity.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BaseCommunity.kt deleted file mode 100644 index a48d082a62c..00000000000 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BaseCommunity.kt +++ /dev/null @@ -1,11 +0,0 @@ -package network.loki.messenger.libsession_util.util - -data class BaseCommunityInfo(val baseUrl: String, val room: String, val pubKeyHex: String) { - companion object { - init { - System.loadLibrary("session_util") - } - external fun parseFullUrl(fullUrl: String): Triple? - } - external fun fullUrl(): String -} \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BlindKeyAPI.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BlindKeyAPI.kt deleted file mode 100644 index cd3dac3af2f..00000000000 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BlindKeyAPI.kt +++ /dev/null @@ -1,15 +0,0 @@ -package network.loki.messenger.libsession_util.util - -object BlindKeyAPI { - private val loadLibrary by lazy { - System.loadLibrary("session_util") - } - - init { - // Ensure the library is loaded at initialization - loadLibrary - } - - external fun blindVersionKeyPair(ed25519SecretKey: ByteArray): KeyPair - external fun blindVersionSign(ed25519SecretKey: ByteArray, timestamp: Long): ByteArray -} \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Contact.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Contact.kt deleted file mode 100644 index 7f0c2906471..00000000000 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Contact.kt +++ /dev/null @@ -1,16 +0,0 @@ -package network.loki.messenger.libsession_util.util - -data class Contact( - val id: String, - var name: String = "", - var nickname: String = "", - var approved: Boolean = false, - var approvedMe: Boolean = false, - var blocked: Boolean = false, - var profilePicture: UserPic = UserPic.DEFAULT, - var priority: Long = 0, - var expiryMode: ExpiryMode = ExpiryMode.NONE, -) { - val displayName: String - get() = nickname.ifEmpty { name } -} \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Conversation.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Conversation.kt deleted file mode 100644 index 6f346705f6c..00000000000 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Conversation.kt +++ /dev/null @@ -1,31 +0,0 @@ -package network.loki.messenger.libsession_util.util - -sealed class Conversation { - - abstract var lastRead: Long - abstract var unread: Boolean - - data class OneToOne( - val accountId: String, - override var lastRead: Long, - override var unread: Boolean - ): Conversation() - - data class Community( - val baseCommunityInfo: BaseCommunityInfo, - override var lastRead: Long, - override var unread: Boolean - ) : Conversation() - - data class LegacyGroup( - val groupId: String, - override var lastRead: Long, - override var unread: Boolean - ): Conversation() - - data class ClosedGroup( - val accountId: String, - override var lastRead: Long, - override var unread: Boolean - ): Conversation() -} \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/ExpiryMode.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/ExpiryMode.kt deleted file mode 100644 index 90947545641..00000000000 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/ExpiryMode.kt +++ /dev/null @@ -1,17 +0,0 @@ -package network.loki.messenger.libsession_util.util - -import kotlin.time.Duration.Companion.seconds - -sealed class ExpiryMode(val expirySeconds: Long) { - object NONE: ExpiryMode(0) - data class AfterSend(private val seconds: Long = 0L): ExpiryMode(seconds) - data class AfterRead(private val seconds: Long = 0L): ExpiryMode(seconds) - - val duration get() = expirySeconds.seconds - - val expiryMillis get() = expirySeconds * 1000L - - fun coerceSendToRead(coerce: Boolean = true) = if (coerce && this is AfterSend) AfterRead(expirySeconds) else this -} - -fun afterSend(seconds: Long) = seconds.takeIf { it > 0 }?.let(ExpiryMode::AfterSend) ?: ExpiryMode.NONE \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/GroupInfo.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/GroupInfo.kt deleted file mode 100644 index 21aa9ad9fba..00000000000 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/GroupInfo.kt +++ /dev/null @@ -1,94 +0,0 @@ -package network.loki.messenger.libsession_util.util - -import org.session.libsignal.utilities.AccountId - -sealed class GroupInfo { - - data class CommunityGroupInfo(val community: BaseCommunityInfo, val priority: Long) : GroupInfo() - - data class ClosedGroupInfo( - val groupAccountId: AccountId, - val adminKey: ByteArray?, - val authData: ByteArray?, - val priority: Long, - val invited: Boolean, - val name: String, - val kicked: Boolean, - val destroyed: Boolean, - val joinedAtSecs: Long - ): GroupInfo() { - init { - require(adminKey == null || adminKey.isNotEmpty()) { - "Admin key must be non-empty if present" - } - - require(authData == null || authData.isNotEmpty()) { - "Auth data must be non-empty if present" - } - } - - fun hasAdminKey() = adminKey != null - - val shouldPoll: Boolean - get() = !invited && !kicked && !destroyed - - companion object { - /** - * Generate the group's admin key(64 bytes) from seed (32 bytes, normally used - * in group promotions). - * - * Use of JvmStatic makes the JNI signature less esoteric. - */ - @JvmStatic - external fun adminKeyFromSeed(seed: ByteArray): ByteArray - } - } - - data class LegacyGroupInfo( - val accountId: String, - val name: String, - val members: Map, - val encPubKey: ByteArray, - val encSecKey: ByteArray, - val priority: Long, - val disappearingTimer: Long, - val joinedAtSecs: Long - ): GroupInfo() { - companion object { - @Suppress("FunctionName") - external fun NAME_MAX_LENGTH(): Int - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as LegacyGroupInfo - - if (accountId != other.accountId) return false - if (name != other.name) return false - if (members != other.members) return false - if (!encPubKey.contentEquals(other.encPubKey)) return false - if (!encSecKey.contentEquals(other.encSecKey)) return false - if (priority != other.priority) return false - if (disappearingTimer != other.disappearingTimer) return false - if (joinedAtSecs != other.joinedAtSecs) return false - - return true - } - - override fun hashCode(): Int { - var result = accountId.hashCode() - result = 31 * result + name.hashCode() - result = 31 * result + members.hashCode() - result = 31 * result + encPubKey.contentHashCode() - result = 31 * result + encSecKey.contentHashCode() - result = 31 * result + priority.hashCode() - result = 31 * result + disappearingTimer.hashCode() - result = 31 * result + joinedAtSecs.hashCode() - return result - } - - } - -} \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/GroupMember.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/GroupMember.kt deleted file mode 100644 index 51afe478099..00000000000 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/GroupMember.kt +++ /dev/null @@ -1,99 +0,0 @@ -package network.loki.messenger.libsession_util.util - -import org.session.libsignal.utilities.AccountId -import java.util.EnumSet - -/** - * Represents a member of a group. - * - * Note: unlike a read-only data class, this class is mutable and it is not thread-safe - * in general. You have to synchronize access to it if you are going to use it in multiple threads. - */ -class GroupMember private constructor( - // Constructed and used by native code. - @Suppress("CanBeParameter") private val nativePtr: Long -) { - init { - if (nativePtr == 0L) { - throw NullPointerException("Native pointer is null") - } - } - - external fun setInvited() - external fun setInviteSent() - external fun setInviteFailed() - external fun setInviteAccepted() - - external fun setPromoted() - external fun setPromotionSent() - external fun setPromotionFailed() - external fun setPromotionAccepted() - - external fun setRemoved(alsoRemoveMessages: Boolean) - - external fun profilePic(): UserPic? - external fun setProfilePic(pic: UserPic) - - external fun setName(name: String) - - private external fun nameString(): String - val name: String get() = nameString() - - private external fun isAdmin(): Boolean - val admin: Boolean get() = isAdmin() - - private external fun isSupplement(): Boolean - external fun setSupplement(supplement: Boolean) - val supplement: Boolean get() = isSupplement() - - external fun accountIdString(): String - val accountId: AccountId get() = AccountId(accountIdString()) - - // The destruction of the native object is called by the GC - // Ideally we want to expose as Closable, however given the tiny footprint of the native object, - // it's perfectly ok to let the GC handle it. - private external fun destroy() - protected fun finalize() { - destroy() - } - - fun isRemoved(status: Status): Boolean { - return status in EnumSet.of(Status.REMOVED, Status.REMOVED_UNKNOWN, Status.REMOVED_INCLUDING_MESSAGES) - } - - fun isAdminOrBeingPromoted(status: Status): Boolean { - return admin || status in EnumSet.of(Status.PROMOTION_SENT, Status.PROMOTION_ACCEPTED) - } - - fun inviteFailed(status: Status): Boolean { - return status == Status.INVITE_FAILED - } - - fun shouldRemoveMessages(status: Status): Boolean { - return status == Status.REMOVED_INCLUDING_MESSAGES - } - - enum class Status(val nativeValue: Int) { - INVITE_UNKNOWN(0), - INVITE_NOT_SENT(1), - INVITE_SENDING(2), - INVITE_FAILED(3), - INVITE_SENT(4), - INVITE_ACCEPTED(5), - - PROMOTION_UNKNOWN(6), - PROMOTION_NOT_SENT(7), - PROMOTION_SENDING(8), - PROMOTION_FAILED(9), - PROMOTION_SENT(10), - PROMOTION_ACCEPTED(11), - - REMOVED_UNKNOWN(12), - REMOVED(13), - REMOVED_INCLUDING_MESSAGES(14); - } - - override fun toString(): String { - return "GroupMember(name=$name, admin=$admin, supplement=$supplement)" - } -} \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Logger.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Logger.kt deleted file mode 100644 index afb20e23ee6..00000000000 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Logger.kt +++ /dev/null @@ -1,11 +0,0 @@ -package network.loki.messenger.libsession_util.util - -object Logger { - - init { - System.loadLibrary("session_util") - } - - @JvmStatic - external fun initLogger() -} \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Sodium.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Sodium.kt deleted file mode 100644 index a7f7e6f1c26..00000000000 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Sodium.kt +++ /dev/null @@ -1,27 +0,0 @@ -package network.loki.messenger.libsession_util.util - - -object Sodium { - - const val KICKED_DOMAIN = "SessionGroupKickedMessage" - - init { - System.loadLibrary("session_util") - } - external fun ed25519KeyPair(seed: ByteArray): KeyPair - external fun ed25519PkToCurve25519(pk: ByteArray): ByteArray - - external fun encryptForMultipleSimple( - messages: Array, - recipients: Array, - ed25519SecretKey: ByteArray, - domain: String - ): ByteArray - - external fun decryptForMultipleSimple( - encoded: ByteArray, - ed25519SecretKey: ByteArray, - senderPubKey: ByteArray, - domain: String, - ): ByteArray? -} \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Utils.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Utils.kt deleted file mode 100644 index 4222395b5de..00000000000 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/Utils.kt +++ /dev/null @@ -1,67 +0,0 @@ -package network.loki.messenger.libsession_util.util - -data class ConfigPush(val config: ByteArray, val seqNo: Long, val obsoleteHashes: List) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as ConfigPush - - if (!config.contentEquals(other.config)) return false - if (seqNo != other.seqNo) return false - if (obsoleteHashes != other.obsoleteHashes) return false - - return true - } - - override fun hashCode(): Int { - var result = config.contentHashCode() - result = 31 * result + seqNo.hashCode() - result = 31 * result + obsoleteHashes.hashCode() - return result - } - -} - -data class UserPic(val url: String, val key: ByteArray) { - companion object { - val DEFAULT = UserPic("", byteArrayOf()) - } - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as UserPic - - if (url != other.url) return false - if (!key.contentEquals(other.key)) return false - - return true - } - - override fun hashCode(): Int { - var result = url.hashCode() - result = 31 * result + key.contentHashCode() - return result - } -} - -data class KeyPair(val pubKey: ByteArray, val secretKey: ByteArray) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as KeyPair - - if (!pubKey.contentEquals(other.pubKey)) return false - if (!secretKey.contentEquals(other.secretKey)) return false - - return true - } - - override fun hashCode(): Int { - var result = pubKey.contentHashCode() - result = 31 * result + secretKey.contentHashCode() - return result - } -} \ No newline at end of file diff --git a/libsession-util/src/test/java/network/loki/messenger/libsession_util/ExampleUnitTest.kt b/libsession-util/src/test/java/network/loki/messenger/libsession_util/ExampleUnitTest.kt deleted file mode 100644 index 3d156bfd4d7..00000000000 --- a/libsession-util/src/test/java/network/loki/messenger/libsession_util/ExampleUnitTest.kt +++ /dev/null @@ -1,14 +0,0 @@ -package network.loki.messenger.libsession_util - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - -} \ No newline at end of file diff --git a/libsession-util/src/test/java/org/session/libsignal/crypto/MnemonicCodecTest.kt b/libsession-util/src/test/java/org/session/libsignal/crypto/MnemonicCodecTest.kt deleted file mode 100644 index cbf3458f0d4..00000000000 --- a/libsession-util/src/test/java/org/session/libsignal/crypto/MnemonicCodecTest.kt +++ /dev/null @@ -1,116 +0,0 @@ -package org.session.libsignal.crypto - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertThrows -import org.junit.Before -import org.junit.Test -import org.session.libsignal.crypto.MnemonicCodec.DecodingError.InputTooShort -import org.session.libsignal.crypto.MnemonicCodec.DecodingError.InvalidWord -import org.session.libsignal.crypto.MnemonicCodec.DecodingError.VerificationFailed -import org.session.libsignal.utilities.Hex - -private val WORD_SET = "abbey,abducts,ability,ablaze,abnormal,abort,abrasive,absorb,abyss,academy,aces,aching,acidic,acoustic,acquire,across,actress,acumen,adapt,addicted,adept,adhesive,adjust,adopt,adrenalin,adult,adventure,aerial,afar,affair,afield,afloat,afoot,afraid,after,against,agenda,aggravate,agile,aglow,agnostic,agony,agreed,ahead,aided,ailments,aimless,airport,aisle,ajar,akin,alarms,album,alchemy,alerts,algebra,alkaline,alley,almost,aloof,alpine,already,also,altitude,alumni,always,amaze,ambush,amended,amidst,ammo,amnesty,among,amply,amused,anchor,android,anecdote,angled,ankle,annoyed,answers,antics,anvil,anxiety,anybody,apart,apex,aphid,aplomb,apology,apply,apricot,aptitude,aquarium,arbitrary,archer,ardent,arena,argue,arises,army,around,arrow,arsenic,artistic,ascend,ashtray,aside,asked,asleep,aspire,assorted,asylum,athlete,atlas,atom,atrium,attire,auburn,auctions,audio,august,aunt,austere,autumn,avatar,avidly,avoid,awakened,awesome,awful,awkward,awning,awoken,axes,axis,axle,aztec,azure,baby,bacon,badge,baffles,bagpipe,bailed,bakery,balding,bamboo,banjo,baptism,basin,batch,bawled,bays,because,beer,befit,begun,behind,being,below,bemused,benches,berries,bested,betting,bevel,beware,beyond,bias,bicycle,bids,bifocals,biggest,bikini,bimonthly,binocular,biology,biplane,birth,biscuit,bite,biweekly,blender,blip,bluntly,boat,bobsled,bodies,bogeys,boil,boldly,bomb,border,boss,both,bounced,bovine,bowling,boxes,boyfriend,broken,brunt,bubble,buckets,budget,buffet,bugs,building,bulb,bumper,bunch,business,butter,buying,buzzer,bygones,byline,bypass,cabin,cactus,cadets,cafe,cage,cajun,cake,calamity,camp,candy,casket,catch,cause,cavernous,cease,cedar,ceiling,cell,cement,cent,certain,chlorine,chrome,cider,cigar,cinema,circle,cistern,citadel,civilian,claim,click,clue,coal,cobra,cocoa,code,coexist,coffee,cogs,cohesive,coils,colony,comb,cool,copy,corrode,costume,cottage,cousin,cowl,criminal,cube,cucumber,cuddled,cuffs,cuisine,cunning,cupcake,custom,cycling,cylinder,cynical,dabbing,dads,daft,dagger,daily,damp,dangerous,dapper,darted,dash,dating,dauntless,dawn,daytime,dazed,debut,decay,dedicated,deepest,deftly,degrees,dehydrate,deity,dejected,delayed,demonstrate,dented,deodorant,depth,desk,devoid,dewdrop,dexterity,dialect,dice,diet,different,digit,dilute,dime,dinner,diode,diplomat,directed,distance,ditch,divers,dizzy,doctor,dodge,does,dogs,doing,dolphin,domestic,donuts,doorway,dormant,dosage,dotted,double,dove,down,dozen,dreams,drinks,drowning,drunk,drying,dual,dubbed,duckling,dude,duets,duke,dullness,dummy,dunes,duplex,duration,dusted,duties,dwarf,dwelt,dwindling,dying,dynamite,dyslexic,each,eagle,earth,easy,eating,eavesdrop,eccentric,echo,eclipse,economics,ecstatic,eden,edgy,edited,educated,eels,efficient,eggs,egotistic,eight,either,eject,elapse,elbow,eldest,eleven,elite,elope,else,eluded,emails,ember,emerge,emit,emotion,empty,emulate,energy,enforce,enhanced,enigma,enjoy,enlist,enmity,enough,enraged,ensign,entrance,envy,epoxy,equip,erase,erected,erosion,error,eskimos,espionage,essential,estate,etched,eternal,ethics,etiquette,evaluate,evenings,evicted,evolved,examine,excess,exhale,exit,exotic,exquisite,extra,exult,fabrics,factual,fading,fainted,faked,fall,family,fancy,farming,fatal,faulty,fawns,faxed,fazed,feast,february,federal,feel,feline,females,fences,ferry,festival,fetches,fever,fewest,fiat,fibula,fictional,fidget,fierce,fifteen,fight,films,firm,fishing,fitting,five,fixate,fizzle,fleet,flippant,flying,foamy,focus,foes,foggy,foiled,folding,fonts,foolish,fossil,fountain,fowls,foxes,foyer,framed,friendly,frown,fruit,frying,fudge,fuel,fugitive,fully,fuming,fungal,furnished,fuselage,future,fuzzy,gables,gadget,gags,gained,galaxy,gambit,gang,gasp,gather,gauze,gave,gawk,gaze,gearbox,gecko,geek,gels,gemstone,general,geometry,germs,gesture,getting,geyser,ghetto,ghost,giant,giddy,gifts,gigantic,gills,gimmick,ginger,girth,giving,glass,gleeful,glide,gnaw,gnome,goat,goblet,godfather,goes,goggles,going,goldfish,gone,goodbye,gopher,gorilla,gossip,gotten,gourmet,governing,gown,greater,grunt,guarded,guest,guide,gulp,gumball,guru,gusts,gutter,guys,gymnast,gypsy,gyrate,habitat,hacksaw,haggled,hairy,hamburger,happens,hashing,hatchet,haunted,having,hawk,haystack,hazard,hectare,hedgehog,heels,hefty,height,hemlock,hence,heron,hesitate,hexagon,hickory,hiding,highway,hijack,hiker,hills,himself,hinder,hippo,hire,history,hitched,hive,hoax,hobby,hockey,hoisting,hold,honked,hookup,hope,hornet,hospital,hotel,hounded,hover,howls,hubcaps,huddle,huge,hull,humid,hunter,hurried,husband,huts,hybrid,hydrogen,hyper,iceberg,icing,icon,identity,idiom,idled,idols,igloo,ignore,iguana,illness,imagine,imbalance,imitate,impel,inactive,inbound,incur,industrial,inexact,inflamed,ingested,initiate,injury,inkling,inline,inmate,innocent,inorganic,input,inquest,inroads,insult,intended,inundate,invoke,inwardly,ionic,irate,iris,irony,irritate,island,isolated,issued,italics,itches,items,itinerary,itself,ivory,jabbed,jackets,jaded,jagged,jailed,jamming,january,jargon,jaunt,javelin,jaws,jazz,jeans,jeers,jellyfish,jeopardy,jerseys,jester,jetting,jewels,jigsaw,jingle,jittery,jive,jobs,jockey,jogger,joining,joking,jolted,jostle,journal,joyous,jubilee,judge,juggled,juicy,jukebox,july,jump,junk,jury,justice,juvenile,kangaroo,karate,keep,kennel,kept,kernels,kettle,keyboard,kickoff,kidneys,king,kiosk,kisses,kitchens,kiwi,knapsack,knee,knife,knowledge,knuckle,koala,laboratory,ladder,lagoon,lair,lakes,lamb,language,laptop,large,last,later,launching,lava,lawsuit,layout,lazy,lectures,ledge,leech,left,legion,leisure,lemon,lending,leopard,lesson,lettuce,lexicon,liar,library,licks,lids,lied,lifestyle,light,likewise,lilac,limits,linen,lion,lipstick,liquid,listen,lively,loaded,lobster,locker,lodge,lofty,logic,loincloth,long,looking,lopped,lordship,losing,lottery,loudly,love,lower,loyal,lucky,luggage,lukewarm,lullaby,lumber,lunar,lurk,lush,luxury,lymph,lynx,lyrics,macro,madness,magically,mailed,major,makeup,malady,mammal,maps,masterful,match,maul,maverick,maximum,mayor,maze,meant,mechanic,medicate,meeting,megabyte,melting,memoir,menu,merger,mesh,metro,mews,mice,midst,mighty,mime,mirror,misery,mittens,mixture,moat,mobile,mocked,mohawk,moisture,molten,moment,money,moon,mops,morsel,mostly,motherly,mouth,movement,mowing,much,muddy,muffin,mugged,mullet,mumble,mundane,muppet,mural,musical,muzzle,myriad,mystery,myth,nabbing,nagged,nail,names,nanny,napkin,narrate,nasty,natural,nautical,navy,nearby,necklace,needed,negative,neither,neon,nephew,nerves,nestle,network,neutral,never,newt,nexus,nibs,niche,niece,nifty,nightly,nimbly,nineteen,nirvana,nitrogen,nobody,nocturnal,nodes,noises,nomad,noodles,northern,nostril,noted,nouns,novelty,nowhere,nozzle,nuance,nucleus,nudged,nugget,nuisance,null,number,nuns,nurse,nutshell,nylon,oaks,oars,oasis,oatmeal,obedient,object,obliged,obnoxious,observant,obtains,obvious,occur,ocean,october,odds,odometer,offend,often,oilfield,ointment,okay,older,olive,olympics,omega,omission,omnibus,onboard,oncoming,oneself,ongoing,onion,online,onslaught,onto,onward,oozed,opacity,opened,opposite,optical,opus,orange,orbit,orchid,orders,organs,origin,ornament,orphans,oscar,ostrich,otherwise,otter,ouch,ought,ounce,ourselves,oust,outbreak,oval,oven,owed,owls,owner,oxidant,oxygen,oyster,ozone,pact,paddles,pager,pairing,palace,pamphlet,pancakes,paper,paradise,pastry,patio,pause,pavements,pawnshop,payment,peaches,pebbles,peculiar,pedantic,peeled,pegs,pelican,pencil,people,pepper,perfect,pests,petals,phase,pheasants,phone,phrases,physics,piano,picked,pierce,pigment,piloted,pimple,pinched,pioneer,pipeline,pirate,pistons,pitched,pivot,pixels,pizza,playful,pledge,pliers,plotting,plus,plywood,poaching,pockets,podcast,poetry,point,poker,polar,ponies,pool,popular,portents,possible,potato,pouch,poverty,powder,pram,present,pride,problems,pruned,prying,psychic,public,puck,puddle,puffin,pulp,pumpkins,punch,puppy,purged,push,putty,puzzled,pylons,pyramid,python,queen,quick,quote,rabbits,racetrack,radar,rafts,rage,railway,raking,rally,ramped,randomly,rapid,rarest,rash,rated,ravine,rays,razor,react,rebel,recipe,reduce,reef,refer,regular,reheat,reinvest,rejoices,rekindle,relic,remedy,renting,reorder,repent,request,reruns,rest,return,reunion,revamp,rewind,rhino,rhythm,ribbon,richly,ridges,rift,rigid,rims,ringing,riots,ripped,rising,ritual,river,roared,robot,rockets,rodent,rogue,roles,romance,roomy,roped,roster,rotate,rounded,rover,rowboat,royal,ruby,rudely,ruffled,rugged,ruined,ruling,rumble,runway,rural,rustled,ruthless,sabotage,sack,sadness,safety,saga,sailor,sake,salads,sample,sanity,sapling,sarcasm,sash,satin,saucepan,saved,sawmill,saxophone,sayings,scamper,scenic,school,science,scoop,scrub,scuba,seasons,second,sedan,seeded,segments,seismic,selfish,semifinal,sensible,september,sequence,serving,session,setup,seventh,sewage,shackles,shelter,shipped,shocking,shrugged,shuffled,shyness,siblings,sickness,sidekick,sieve,sifting,sighting,silk,simplest,sincerely,sipped,siren,situated,sixteen,sizes,skater,skew,skirting,skulls,skydive,slackens,sleepless,slid,slower,slug,smash,smelting,smidgen,smog,smuggled,snake,sneeze,sniff,snout,snug,soapy,sober,soccer,soda,software,soggy,soil,solved,somewhere,sonic,soothe,soprano,sorry,southern,sovereign,sowed,soya,space,speedy,sphere,spiders,splendid,spout,sprig,spud,spying,square,stacking,stellar,stick,stockpile,strained,stunning,stylishly,subtly,succeed,suddenly,suede,suffice,sugar,suitcase,sulking,summon,sunken,superior,surfer,sushi,suture,swagger,swept,swiftly,sword,swung,syllabus,symptoms,syndrome,syringe,system,taboo,tacit,tadpoles,tagged,tail,taken,talent,tamper,tanks,tapestry,tarnished,tasked,tattoo,taunts,tavern,tawny,taxi,teardrop,technical,tedious,teeming,tell,template,tender,tepid,tequila,terminal,testing,tether,textbook,thaw,theatrics,thirsty,thorn,threaten,thumbs,thwart,ticket,tidy,tiers,tiger,tilt,timber,tinted,tipsy,tirade,tissue,titans,toaster,tobacco,today,toenail,toffee,together,toilet,token,tolerant,tomorrow,tonic,toolbox,topic,torch,tossed,total,touchy,towel,toxic,toyed,trash,trendy,tribal,trolling,truth,trying,tsunami,tubes,tucks,tudor,tuesday,tufts,tugs,tuition,tulips,tumbling,tunnel,turnip,tusks,tutor,tuxedo,twang,tweezers,twice,twofold,tycoon,typist,tyrant,ugly,ulcers,ultimate,umbrella,umpire,unafraid,unbending,uncle,under,uneven,unfit,ungainly,unhappy,union,unjustly,unknown,unlikely,unmask,unnoticed,unopened,unplugs,unquoted,unrest,unsafe,until,unusual,unveil,unwind,unzip,upbeat,upcoming,update,upgrade,uphill,upkeep,upload,upon,upper,upright,upstairs,uptight,upwards,urban,urchins,urgent,usage,useful,usher,using,usual,utensils,utility,utmost,utopia,uttered,vacation,vague,vain,value,vampire,vane,vapidly,vary,vastness,vats,vaults,vector,veered,vegan,vehicle,vein,velvet,venomous,verification,vessel,veteran,vexed,vials,vibrate,victim,video,viewpoint,vigilant,viking,village,vinegar,violin,vipers,virtual,visited,vitals,vivid,vixen,vocal,vogue,voice,volcano,vortex,voted,voucher,vowels,voyage,vulture,wade,waffle,wagtail,waist,waking,wallets,wanted,warped,washing,water,waveform,waxing,wayside,weavers,website,wedge,weekday,weird,welders,went,wept,were,western,wetsuit,whale,when,whipped,whole,wickets,width,wield,wife,wiggle,wildly,winter,wipeout,wiring,wise,withdrawn,wives,wizard,wobbly,woes,woken,wolf,womanly,wonders,woozy,worry,wounded,woven,wrap,wrist,wrong,yacht,yahoo,yanks,yard,yawning,yearbook,yellow,yesterday,yeti,yields,yodel,yoga,younger,yoyo,zapped,zeal,zebra,zero,zesty,zigzags,zinger,zippers,zodiac,zombie,zones,zoom" - -class MnemonicCodecTest { - lateinit var codec: MnemonicCodec - - @Before - fun setup() { - codec = MnemonicCodec { WORD_SET } - } - - @Test - fun `encode works`() { - val result = codec.encode("e37315dd45b0b0454dbb04dce662772a") - - assertEquals("sewage railway names rift wagtail duties rowboat until seismic radar custom gimmick until", result) - } - - @Test - fun `decode empty`() { - assertThrows(InputTooShort::class.java) { - codec.decode("") - } - } - - @Test - fun `decode one invalid word that is too short`() { - assertThrows(InputTooShort::class.java) { - codec.decode("a") - } - } - - @Test - fun `decode one invalid word`() { - assertThrows(InputTooShort::class.java) { - codec.decode("abcd") - } - } - - @Test - fun `decode one valid word`() { - assertThrows(InputTooShort::class.java) { - codec.decode("abbey") - } - } - - - @Test - fun `decode password with the word organism which is not on the list but organs is - ses-2202`() { - assertThrows(VerificationFailed::class.java) { - codec.decode("dotted bikini vexed vane orbit dabbing diet amidst geek goldfish ceiling organism orbit") - } - } - - @Test - fun `decode works`() { - val result = codec.decode("sewage railway names rift wagtail duties rowboat until seismic radar custom gimmick until") - - assertEquals("e37315dd45b0b0454dbb04dce662772a", result) - } - - @Test - fun `decodeMnemonicOrHexAsByteArray with mnemonic`() { - val result = codec.decodeMnemonicOrHexAsByteArray("fuming nearby kennel husband dejected pepper jaded because dads goggles tufts tomorrow dejected").let(Hex::toStringCondensed) - - assertEquals("0f2ccde528622876b8f16e14db97dafc", result) - } - - @Test - fun `sanitizeAndDecodeAsByteArray with mnemonic with unnecessary spaces`() { - val result = codec.sanitizeAndDecodeAsByteArray(" fuming nearby kennel husband dejected pepper jaded because dads goggles tufts tomorrow dejected ").let(Hex::toStringCondensed) - - assertEquals("0f2ccde528622876b8f16e14db97dafc", result) - } - - @Test - fun `sanitizeAndDecodeAsByteArray with mnemonic with special characters`() { - val result = codec.sanitizeAndDecodeAsByteArray("...fuming nearby.kennel.husband . dejected pepper jaded because dads goggles tufts tomorrow dejected@").let(Hex::toStringCondensed) - - assertEquals("0f2ccde528622876b8f16e14db97dafc", result) - } - - @Test - fun `decodeMnemonicOrHexAsByteArray with hex`() { - val result = codec.decodeMnemonicOrHexAsByteArray("0f2ccde528622876b8f16e14db97dafc").let(Hex::toStringCondensed) - - assertEquals("0f2ccde528622876b8f16e14db97dafc", result) - } - - @Test - fun `decodeMnemonicOrHexAsByteArray with account id throws`() { - assertThrows(InputTooShort::class.java) { - codec.decodeMnemonicOrHexAsByteArray("0582e1421da6f584a4795d30b654b4f25fed860afdf081075cb26a2b997e492f14").let(Hex::toStringCondensed) - } - } - - @Test - fun `decodeMnemonicOrHexAsByteArray with bad hex`() { - // throws InvalidWord as 0f2ccde528622876b8f16e14db97dafcg is not a valid word on the english wordlist. - // It is also not a valid hex string, but we assume that a non-hex string is a recovery password. - - assertThrows(InputTooShort::class.java) { - codec.decodeMnemonicOrHexAsByteArray("0f2ccde528622876b8f16e14db97dafcg").let(Hex::toStringCondensed) - } - } -} diff --git a/libsession/build.gradle b/libsession/build.gradle index 2efccda91ec..0bab7c2493d 100644 --- a/libsession/build.gradle +++ b/libsession/build.gradle @@ -47,13 +47,14 @@ android { dependencies { implementation project(":libsignal") - implementation project(":libsession-util") implementation project(":liblazysodium") implementation("com.google.dagger:hilt-android:$daggerHiltVersion") ksp("com.google.dagger:hilt-compiler:$daggerHiltVersion") ksp("androidx.hilt:hilt-compiler:$jetpackHiltVersion") + api 'org.sessionfoundation:libsession-util-android:1.0.0' + implementation "net.java.dev.jna:jna:5.12.1@aar" implementation "androidx.core:core-ktx:$coreVersion" implementation "androidx.appcompat:appcompat:$appcompatVersion" 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 06ea7032534..19d17f5de7d 100644 --- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -3,7 +3,6 @@ package org.session.libsession.database import android.content.Context import android.net.Uri import com.goterl.lazysodium.utils.KeyPair -import network.loki.messenger.libsession_util.util.GroupDisplayInfo import org.session.libsession.messaging.BlindedIdMapping import org.session.libsession.messaging.calls.CallMessageType import org.session.libsession.messaging.contacts.Contact @@ -29,6 +28,7 @@ import org.session.libsession.messaging.sending_receiving.link_preview.LinkPrevi import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel import org.session.libsession.messaging.utilities.UpdateMessageData import org.session.libsession.utilities.Address +import org.session.libsession.utilities.GroupDisplayInfo import org.session.libsession.utilities.GroupRecord import org.session.libsession.utilities.recipients.MessageType import org.session.libsession.utilities.recipients.Recipient diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt index cfd6ab5d40b..372cd333355 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt @@ -56,7 +56,7 @@ class InviteContactsJob(val groupSessionId: String, val memberSessionIds: Array< // Make the request for this member val memberId = AccountId(memberSessionId) val (groupName, subAccount) = configs.withMutableGroupConfigs(sessionId) { configs -> - configs.groupInfo.getName() to configs.groupKeys.makeSubAccount(memberId) + configs.groupInfo.getName() to configs.groupKeys.makeSubAccount(memberSessionId) } val timestamp = SnodeAPI.nowWithOffset diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index 759c2a7983c..0b4966112c6 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISIBLE +import network.loki.messenger.libsession_util.Namespace import network.loki.messenger.libsession_util.util.ExpiryMode import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred @@ -46,7 +47,6 @@ import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.Namespace import org.session.libsignal.utilities.defaultRequiresAuth import org.session.libsignal.utilities.hasNamespaces import org.session.libsignal.utilities.hexEncodedPublicKey diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/LegacyClosedGroupPollerV2.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/LegacyClosedGroupPollerV2.kt index 2e050514425..f112e89d697 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/LegacyClosedGroupPollerV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/LegacyClosedGroupPollerV2.kt @@ -1,6 +1,7 @@ package org.session.libsession.messaging.sending_receiving.pollers import kotlinx.coroutines.GlobalScope +import network.loki.messenger.libsession_util.Namespace import nl.komponents.kovenant.Promise import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.map @@ -15,7 +16,6 @@ import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.GroupUtil import org.session.libsignal.crypto.secureRandomOrNull import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.Namespace import org.session.libsignal.utilities.defaultRequiresAuth import org.session.libsignal.utilities.hasNamespaces import java.text.DateFormat diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt index d99379f877d..79faea15e14 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt @@ -19,6 +19,7 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import network.loki.messenger.libsession_util.Namespace import nl.komponents.kovenant.Deferred import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred @@ -44,9 +45,7 @@ import org.session.libsession.utilities.UserConfigType import org.session.libsignal.database.LokiAPIDatabaseProtocol import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.Namespace import org.session.libsignal.utilities.Snode -import org.session.libsignal.utilities.Util.SECURE_RANDOM private const val TAG = "Poller" diff --git a/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt b/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt index 8075216db1f..9533b5cc891 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt @@ -13,6 +13,7 @@ import network.loki.messenger.libsession_util.MutableGroupKeysConfig import network.loki.messenger.libsession_util.MutableGroupMembersConfig import network.loki.messenger.libsession_util.MutableUserGroupsConfig import network.loki.messenger.libsession_util.MutableUserProfile +import network.loki.messenger.libsession_util.Namespace import network.loki.messenger.libsession_util.ReadableConfig import network.loki.messenger.libsession_util.ReadableContacts import network.loki.messenger.libsession_util.ReadableConversationVolatileConfig @@ -26,7 +27,6 @@ import network.loki.messenger.libsession_util.util.GroupInfo import org.session.libsession.snode.SwarmAuth import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.AccountId -import org.session.libsignal.utilities.Namespace interface ConfigFactoryProtocol { val configUpdateNotifications: Flow diff --git a/libsession/src/main/java/org/session/libsession/utilities/ConfigUtils.kt b/libsession/src/main/java/org/session/libsession/utilities/ConfigUtils.kt new file mode 100644 index 00000000000..2b8813bcaf3 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/utilities/ConfigUtils.kt @@ -0,0 +1,21 @@ +package org.session.libsession.utilities + +import network.loki.messenger.libsession_util.MutableContacts +import network.loki.messenger.libsession_util.util.Contact +import org.session.libsignal.utilities.IdPrefix +import org.session.libsignal.utilities.Log + +/** + * This function will create the underlying contact if it doesn't exist before passing to [updateFunction] + */ +fun MutableContacts.upsertContact(accountId: String, updateFunction: Contact.() -> Unit = {}) { + when { + accountId.startsWith(IdPrefix.BLINDED.value) -> Log.w("Loki", "Trying to create a contact with a blinded ID prefix") + accountId.startsWith(IdPrefix.UN_BLINDED.value) -> Log.w("Loki", "Trying to create a contact with an un-blinded ID prefix") + accountId.startsWith(IdPrefix.BLINDEDV2.value) -> Log.w("Loki", "Trying to create a contact with a blindedv2 ID prefix") + else -> getOrConstruct(accountId).let { + updateFunction(it) + set(it) + } + } +} \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/GroupDisplayInfo.kt b/libsession/src/main/java/org/session/libsession/utilities/GroupDisplayInfo.kt similarity index 74% rename from libsession-util/src/main/java/network/loki/messenger/libsession_util/util/GroupDisplayInfo.kt rename to libsession/src/main/java/org/session/libsession/utilities/GroupDisplayInfo.kt index 808ca331c5f..285c04e8c98 100644 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/GroupDisplayInfo.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/GroupDisplayInfo.kt @@ -1,5 +1,6 @@ -package network.loki.messenger.libsession_util.util +package org.session.libsession.utilities +import network.loki.messenger.libsession_util.util.UserPic import org.session.libsignal.utilities.AccountId data class GroupDisplayInfo( diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/Namespace.kt b/libsignal/src/main/java/org/session/libsignal/utilities/Namespace.kt deleted file mode 100644 index 1c39abb47fe..00000000000 --- a/libsignal/src/main/java/org/session/libsignal/utilities/Namespace.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.session.libsignal.utilities - -object Namespace { - fun ALL() = "all" - - // Namespaces used for legacy group - fun UNAUTHENTICATED_CLOSED_GROUP() = -10 - - // Namespaces used for user's own swarm - external fun DEFAULT(): Int - external fun USER_PROFILE(): Int - external fun CONTACTS(): Int - external fun CONVO_INFO_VOLATILE(): Int - external fun USER_GROUPS(): Int - - // Namesapced used for groupv2 - external fun GROUP_INFO(): Int - external fun GROUP_MEMBERS(): Int - external fun GROUP_KEYS(): Int - external fun GROUP_MESSAGES(): Int - external fun REVOKED_GROUP_MESSAGES(): Int -} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 76c4463421e..d93906a4f20 100644 --- a/settings.gradle +++ b/settings.gradle @@ -12,5 +12,4 @@ include ':app' include ':liblazysodium' include ':libsession' include ':libsignal' -include ':libsession-util' include ':content-descriptions' // ONLY AccessibilityID strings (non-translated) used to identify UI elements in automated testing From 6a7e0f7885b24ae3da55b3a9239239396b52863c Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 28 Mar 2025 09:48:36 +1030 Subject: [PATCH 02/43] Making sure the path activity renders well o n small screens (#1061) * Making sure the path activity renders well o n small screens * Tweaked the function * Removed commented code --- .../securesms/home/PathActivity.kt | 17 ++++++++++++ app/src/main/res/drawable/fade_gradient.xml | 12 +++++++++ app/src/main/res/layout/activity_path.xml | 27 ++++++++++++++----- 3 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 app/src/main/res/drawable/fade_gradient.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/PathActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/PathActivity.kt index 96fd06396f4..af763b0500b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/PathActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/PathActivity.kt @@ -10,12 +10,14 @@ import android.util.AttributeSet import android.util.TypedValue import android.view.Gravity import android.view.View +import android.widget.FrameLayout import android.widget.LinearLayout import android.widget.RelativeLayout import android.widget.TextView import android.widget.Toast import androidx.annotation.ColorRes import androidx.core.content.ContextCompat +import androidx.core.view.doOnLayout import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle @@ -49,6 +51,7 @@ import org.thoughtcrime.securesms.util.fadeIn import org.thoughtcrime.securesms.util.fadeOut import org.thoughtcrime.securesms.util.getAccentColor + class PathActivity : ScreenLockActionBarActivity() { private lateinit var binding: ActivityPathBinding private val broadcastReceivers = mutableListOf() @@ -82,6 +85,20 @@ class PathActivity : ScreenLockActionBarActivity() { } } } + + binding.pathScroll.doOnLayout { + val child: View = binding.pathScroll.getChildAt(0) + val isScrollable: Boolean = child.height > binding.pathScroll.height + val params = binding.pathRowsContainer.layoutParams as FrameLayout.LayoutParams + + if(isScrollable){ + params.gravity = Gravity.CENTER_HORIZONTAL + } else { + params.gravity = Gravity.CENTER + } + + binding.pathRowsContainer.layoutParams = params + } } private fun registerObservers() { diff --git a/app/src/main/res/drawable/fade_gradient.xml b/app/src/main/res/drawable/fade_gradient.xml new file mode 100644 index 00000000000..0a4c6bc9de1 --- /dev/null +++ b/app/src/main/res/drawable/fade_gradient.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_path.xml b/app/src/main/res/layout/activity_path.xml index b91b0ccbe92..647bd6a7d08 100644 --- a/app/src/main/res/layout/activity_path.xml +++ b/app/src/main/res/layout/activity_path.xml @@ -27,12 +27,27 @@ android:layout_margin="@dimen/large_spacing" android:clipChildren="false"> - + + + + + + + Date: Fri, 28 Mar 2025 10:13:58 +1030 Subject: [PATCH 03/43] Feature/expired attachments (#1037) * Adding an expired state for image attachments * UI updates Pending/Expired attachments now use different icons for different types Consistent padding and styling for bubbled control messages (call cm, deleted message, pending/expired attachments) * Fixing pending/expired view styling * Catering for expired 404 attachments * Removing the duplication of attachment state. Relying only on the enum * Handling video icon for attachment control * Renamed Pending view to AttachmentControl as it will have more responsibility now Handling loading and failed state in attachment controls * Properly handling quotes for attachment types and states * Quote styling fixes * Message UI tweaking * Retry on tap for failed attachments * Retrying failed attachments * Renamind pending attachment download * Removing redundant state in attachment controls * Catering for multi image attachment control and calculating total attachment size * Making sure retry works for multiple attachments * Showing attachment controls in the message details instead of the carousel if the image isn't downloaded * Making sure we show videos in the message details page * Fixing audio slide issue * Making sure we can interact with pending and failed attachments from the message details * Message details show state update when downloading a pending attachment Also added a new debug menu item to "untrust" attachments * crowdin strings * Added a todo * PR feedback --- .../MmsNotificationAttachment.java | 14 +- .../v2/AttachmentDownloadHandler.kt | 25 ++- .../conversation/v2/ConversationActivityV2.kt | 3 +- .../conversation/v2/ConversationAdapter.kt | 6 +- .../conversation/v2/ConversationViewModel.kt | 8 +- .../conversation/v2/MessageDetailActivity.kt | 58 ++++-- .../v2/MessageDetailsViewModel.kt | 195 ++++++++++-------- .../v2/components/AlbumThumbnailView.kt | 9 +- .../v2/messages/AttachmentControlView.kt | 175 ++++++++++++++++ .../v2/messages/DeletedMessageView.kt | 5 +- .../conversation/v2/messages/DocumentView.kt | 3 + .../v2/messages/LinkPreviewView.kt | 1 - .../v2/messages/PendingAttachmentView.kt | 65 ------ .../conversation/v2/messages/QuoteView.kt | 42 ++-- .../v2/messages/VisibleMessageContentView.kt | 189 ++++++++++++----- .../v2/messages/VisibleMessageView.kt | 6 +- .../v2/utilities/ThumbnailView.kt | 13 +- .../database/AttachmentDatabase.java | 80 ++++--- .../database/model/MmsMessageRecord.java | 14 ++ .../securesms/debugmenu/DebugMenu.kt | 9 + .../securesms/debugmenu/DebugMenuViewModel.kt | 53 ++++- .../securesms/home/SeedReminder.kt | 1 - .../linkpreview/LinkPreviewRepository.java | 4 +- .../securesms/media/MediaOverviewScreen.kt | 2 +- .../securesms/media/MediaOverviewTopAppBar.kt | 25 +++ .../securesms/media/MediaOverviewViewModel.kt | 4 - .../thoughtcrime/securesms/mms/AudioSlide.kt | 6 +- .../org/thoughtcrime/securesms/mms/Slide.kt | 21 +- .../thoughtcrime/securesms/mms/SlideDeck.java | 10 + .../securesms/ui/components/AppBar.kt | 19 +- .../securesms/util/AttachmentUtil.kt | 22 +- .../res/drawable/call_message_background.xml | 9 - .../drawable/ic_download_circle_filled_48.xml | 10 - ...=> message_bubble_background_received.xml} | 2 +- .../main/res/layout/dialog_clear_all_data.xml | 1 - app/src/main/res/layout/thumbnail_view.xml | 18 -- .../res/layout/view_attachment_control.xml | 71 +++++++ .../main/res/layout/view_control_message.xml | 2 + .../view_conversation_typing_container.xml | 3 +- .../main/res/layout/view_deleted_message.xml | 16 +- app/src/main/res/layout/view_document.xml | 67 ++++-- .../res/layout/view_open_group_invitation.xml | 6 +- .../res/layout/view_pending_attachment.xml | 49 ----- app/src/main/res/layout/view_quote.xml | 4 +- .../main/res/layout/view_visible_message.xml | 12 +- .../layout/view_visible_message_content.xml | 27 +-- app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/styles.xml | 13 +- .../messaging/jobs/AttachmentDownloadJob.kt | 21 +- .../jobs/RetrieveProfileAvatarJob.kt | 13 +- .../attachments/Attachment.java | 12 +- .../attachments/AttachmentTransferProgress.kt | 8 - .../attachments/PointerAttachment.java | 56 ++--- .../attachments/SessionServiceAttachment.kt | 6 +- .../session/libsession/utilities/Contact.java | 4 +- .../libsession/utilities/DownloadUtilities.kt | 3 - 56 files changed, 979 insertions(+), 542 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/AttachmentControlView.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/PendingAttachmentView.kt delete mode 100644 app/src/main/res/drawable/call_message_background.xml delete mode 100644 app/src/main/res/drawable/ic_download_circle_filled_48.xml rename app/src/main/res/drawable/{message_bubble_background_received_alone.xml => message_bubble_background_received.xml} (79%) create mode 100644 app/src/main/res/layout/view_attachment_control.xml delete mode 100644 app/src/main/res/layout/view_pending_attachment.xml delete mode 100644 libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/AttachmentTransferProgress.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java index feb15a24d53..0124a3d17f4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java @@ -1,15 +1,17 @@ package org.thoughtcrime.securesms.attachments; import android.net.Uri; + import androidx.annotation.Nullable; + import org.session.libsession.messaging.sending_receiving.attachments.Attachment; -import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState; import org.thoughtcrime.securesms.database.MmsDatabase; public class MmsNotificationAttachment extends Attachment { public MmsNotificationAttachment(int status, long size) { - super("application/mms", getTransferStateFromStatus(status), size, null, null, null, null, null, null, false, 0, 0, false, null, ""); + super("application/mms", getTransferStateFromStatus(status).getValue(), size, null, null, null, null, null, null, false, 0, 0, false, null, ""); } @Nullable @@ -20,15 +22,15 @@ public MmsNotificationAttachment(int status, long size) { @Override public Uri getThumbnailUri() { return null; } - private static int getTransferStateFromStatus(int status) { + private static AttachmentState getTransferStateFromStatus(int status) { if (status == MmsDatabase.Status.DOWNLOAD_INITIALIZED || status == MmsDatabase.Status.DOWNLOAD_NO_CONNECTIVITY) { - return AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING; + return AttachmentState.PENDING; } else if (status == MmsDatabase.Status.DOWNLOAD_CONNECTING) { - return AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED; + return AttachmentState.DOWNLOADING; } else { - return AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED; + return AttachmentState.FAILED; } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AttachmentDownloadHandler.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AttachmentDownloadHandler.kt index 7278be71bcd..8c1419c6951 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AttachmentDownloadHandler.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AttachmentDownloadHandler.kt @@ -14,7 +14,7 @@ import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.jobs.AttachmentDownloadJob import org.session.libsession.messaging.jobs.AttachmentUploadJob import org.session.libsession.messaging.jobs.JobQueue -import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.util.flatten @@ -24,14 +24,14 @@ import org.thoughtcrime.securesms.util.timedBuffer * [AttachmentDownloadHandler] is responsible for handling attachment download requests. These * requests will go through different level of checking before they are queued for download. * - * To use this handler, call [onAttachmentDownloadRequest] with the attachment that needs to be - * downloaded. The call to [onAttachmentDownloadRequest] is cheap and can be called multiple times. + * To use this handler, call [downloadPendingAttachment] with the attachment that needs to be + * downloaded. The call to [downloadPendingAttachment] is cheap and can be called multiple times. */ class AttachmentDownloadHandler( private val storage: StorageProtocol, private val messageDataProvider: MessageDataProvider, jobQueue: JobQueue = JobQueue.shared, - scope: CoroutineScope = CoroutineScope(Dispatchers.Default) + SupervisorJob(), + private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default) + SupervisorJob(), ) { companion object { private const val BUFFER_TIMEOUT_MILLS = 500L @@ -101,15 +101,26 @@ class AttachmentDownloadHandler( } - fun onAttachmentDownloadRequest(attachment: DatabaseAttachment) { - if (attachment.transferState != AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING) { + fun downloadPendingAttachment(attachment: DatabaseAttachment) { + if (attachment.transferState != AttachmentState.PENDING.value) { Log.i( LOG_TAG, - "Attachment ${attachment.attachmentId} is not pending, skipping download" + "Attachment ${attachment.attachmentId} is not pending nor failed, skipping download (state = ${attachment.transferState})}" ) return } downloadRequests.trySend(attachment) } + + fun retryFailedAttachments(attachments: List){ + attachments.forEach { attachment -> + if (attachment.transferState != AttachmentState.FAILED.value) return Log.d( + LOG_TAG, + "Attachment ${attachment.attachmentId} is not failed, skipping retry" + ) + + downloadRequests.trySend(attachment) + } + } } 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 d12cc78dc75..4b51ae7dc26 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 @@ -363,7 +363,8 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, onDeselect(message, position, it) } }, - onAttachmentNeedsDownload = viewModel::onAttachmentDownloadRequest, + downloadPendingAttachment = viewModel::downloadPendingAttachment, + retryFailedAttachments = viewModel::retryFailedAttachments, glide = glide, lifecycleCoroutineScope = lifecycleScope ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt index 83577df30e5..5e706156aa8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt @@ -41,7 +41,8 @@ class ConversationAdapter( private val onItemSwipeToReply: (MessageRecord, Int) -> Unit, private val onItemLongPress: (MessageRecord, Int, View) -> Unit, private val onDeselect: (MessageRecord, Int) -> Unit, - private val onAttachmentNeedsDownload: (DatabaseAttachment) -> Unit, + private val downloadPendingAttachment: (DatabaseAttachment) -> Unit, + private val retryFailedAttachments: (List) -> Unit, private val glide: RequestManager, lifecycleCoroutineScope: LifecycleCoroutineScope ) : CursorRecyclerViewAdapter(context, cursor) { @@ -143,7 +144,8 @@ class ConversationAdapter( senderId, lastSeen.get(), visibleMessageViewDelegate, - onAttachmentNeedsDownload + downloadPendingAttachment, + retryFailedAttachments ) if (!message.isDeleted) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index 11e43c70ece..877fbc8a355 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -963,8 +963,12 @@ class ConversationViewModel( storage.getLastLegacyRecipient(address.toString())?.let { Recipient.from(context, Address.fromSerialized(it), false) } } - fun onAttachmentDownloadRequest(attachment: DatabaseAttachment) { - attachmentDownloadHandler.onAttachmentDownloadRequest(attachment) + fun downloadPendingAttachment(attachment: DatabaseAttachment) { + attachmentDownloadHandler.downloadPendingAttachment(attachment) + } + + fun retryFailedAttachments(attachments: List){ + attachmentDownloadHandler.retryFailedAttachments(attachments) } fun beforeSendingTextOnlyMessage() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index afbfa6770b9..b087906b549 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -1,15 +1,13 @@ package org.thoughtcrime.securesms.conversation.v2 import android.annotation.SuppressLint +import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.MotionEvent.ACTION_UP import androidx.activity.viewModels -import androidx.annotation.DrawableRes -import androidx.appcompat.content.res.AppCompatResources -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -54,12 +52,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView +import androidx.fragment.app.DialogFragment import androidx.lifecycle.lifecycleScope import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage -import com.google.accompanist.drawablepainter.rememberDrawablePainter +import com.bumptech.glide.integration.compose.placeholder import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject import kotlinx.coroutines.launch import network.loki.messenger.R import network.loki.messenger.databinding.ViewVisibleMessageContentBinding @@ -67,6 +65,7 @@ import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.thoughtcrime.securesms.MediaPreviewActivity.getPreviewIntent import org.thoughtcrime.securesms.ScreenLockActionBarActivity +import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader import org.thoughtcrime.securesms.ui.Avatar import org.thoughtcrime.securesms.ui.CarouselNextButton import org.thoughtcrime.securesms.ui.CarouselPrevButton @@ -87,9 +86,12 @@ import org.thoughtcrime.securesms.ui.theme.blackAlpha40 import org.thoughtcrime.securesms.ui.theme.bold import org.thoughtcrime.securesms.ui.theme.dangerButtonColors import org.thoughtcrime.securesms.ui.theme.monospace +import org.thoughtcrime.securesms.util.ActivityDispatcher +import org.thoughtcrime.securesms.util.push +import javax.inject.Inject @AndroidEntryPoint -class MessageDetailActivity : ScreenLockActionBarActivity() { +class MessageDetailActivity : ScreenLockActionBarActivity(), ActivityDispatcher { @Inject lateinit var storage: StorageProtocol @@ -128,6 +130,14 @@ class MessageDetailActivity : ScreenLockActionBarActivity() { } } + override fun dispatchIntent(body: (Context) -> Intent?) { + body(this)?.let { push(it, false) } + } + + override fun showDialog(dialogFragment: DialogFragment, tag: String?) { + dialogFragment.show(supportFragmentManager, tag) + } + @Composable private fun MessageDetailsScreen() { val state by viewModel.stateFlow.collectAsState() @@ -144,7 +154,7 @@ class MessageDetailActivity : ScreenLockActionBarActivity() { onDelete = if (state.canDelete) { { setResultAndFinish(ON_DELETE) } } else null, onCopy = { setResultAndFinish(ON_COPY) }, onClickImage = { viewModel.onClickImage(it) }, - onAttachmentNeedsDownload = viewModel::onAttachmentNeedsDownload, + retryFailedAttachments = viewModel::retryFailedAttachments ) } @@ -167,7 +177,7 @@ fun MessageDetails( onDelete: (() -> Unit)? = null, onCopy: () -> Unit = {}, onClickImage: (Int) -> Unit = {}, - onAttachmentNeedsDownload: (DatabaseAttachment) -> Unit = { _ -> } + retryFailedAttachments: (List) -> Unit ) { Column( modifier = Modifier @@ -180,12 +190,20 @@ fun MessageDetails( modifier = Modifier.padding(horizontal = LocalDimensions.current.spacing) ) { AndroidView( - factory = { - ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(it)).mainContainerConstraint.apply { + factory = { context -> + // Inflate the view once + ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(context)).root + }, + update = { view -> + // Rebind the view whenever state changes. + // Retrieve the binding from the view + val binding = ViewVisibleMessageContentBinding.bind(view) + binding.mainContainerConstraint.apply { bind( message, thread = state.thread!!, - onAttachmentNeedsDownload = onAttachmentNeedsDownload, + downloadPendingAttachment = {}, // the view shouldn't handle this from the details activity + retryFailedAttachments = retryFailedAttachments, suppressThumbnails = true ) @@ -349,7 +367,6 @@ fun CellButtons( } } -@OptIn(ExperimentalFoundationApi::class) @Composable fun Carousel(attachments: List, onClick: (Int) -> Unit) { if (attachments.isEmpty()) return @@ -378,7 +395,6 @@ fun Carousel(attachments: List, onClick: (Int) -> Unit) { } @OptIn( - ExperimentalFoundationApi::class, ExperimentalGlideComposeApi::class ) @Composable @@ -397,7 +413,8 @@ private fun CarouselPager( modifier = Modifier .aspectRatio(1f) .clickable { onClick(i) }, - model = attachments[i].uri, + model = if(attachments[i].uri != null) DecryptableStreamUriLoader.DecryptableUri(attachments[i].uri!!) + else null, contentDescription = attachments[i].fileName ?: stringResource(id = R.string.image) ) } @@ -454,7 +471,8 @@ fun PreviewMessageDetails( ), fileName = "Screen Shot 2023-07-06 at 11.35.50 am.png", uri = Uri.parse(""), - hasImage = true + hasImage = true, + isDownloaded = true ), Attachment( fileDetails = listOf( @@ -462,7 +480,8 @@ fun PreviewMessageDetails( ), fileName = "Screen Shot 2023-07-06 at 11.35.50 am.png", uri = Uri.parse(""), - hasImage = true + hasImage = true, + isDownloaded = true ), Attachment( fileDetails = listOf( @@ -470,7 +489,8 @@ fun PreviewMessageDetails( ), fileName = "Screen Shot 2023-07-06 at 11.35.50 am.png", uri = Uri.parse(""), - hasImage = true + hasImage = true, + isDownloaded = true ) ), @@ -484,7 +504,9 @@ fun PreviewMessageDetails( received = TitledText(R.string.received, "6:12 AM Tue, 09/08/2022"), error = TitledText(R.string.error, "Message failed to send"), senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54"), - ) + + ), + retryFailedAttachments = {} ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt index 9f4a500e7b5..0808b9d18ed 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailsViewModel.kt @@ -5,23 +5,21 @@ import androidx.annotation.DrawableRes import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import java.util.Date -import java.util.concurrent.TimeUnit -import javax.inject.Inject -import kotlin.text.Typography.ellipsis import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import network.loki.messenger.R +import org.session.libsession.database.MessageDataProvider import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager -import org.session.libsession.messaging.jobs.AttachmentDownloadJob -import org.session.libsession.messaging.jobs.JobQueue -import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.utilities.Address import org.session.libsession.utilities.TextSecurePreferences @@ -30,8 +28,10 @@ import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.MediaPreviewArgs import org.thoughtcrime.securesms.database.AttachmentDatabase +import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.LokiMessageDatabase import org.thoughtcrime.securesms.database.MmsSmsDatabase +import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord @@ -40,6 +40,12 @@ import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.repository.ConversationRepository import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.TitledText +import org.thoughtcrime.securesms.util.observeChanges +import java.util.Date +import java.util.Locale +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import kotlin.text.Typography.ellipsis @HiltViewModel class MessageDetailsViewModel @Inject constructor( @@ -50,7 +56,9 @@ class MessageDetailsViewModel @Inject constructor( private val threadDb: ThreadDatabase, private val repository: ConversationRepository, private val deprecationManager: LegacyGroupDeprecationManager, - private val context: ApplicationContext + private val context: ApplicationContext, + private val messageDataProvider: MessageDataProvider, + private val storage: Storage ) : ViewModel() { private var job: Job? = null @@ -61,6 +69,13 @@ class MessageDetailsViewModel @Inject constructor( private val event = Channel() val eventFlow = event.receiveAsFlow() + private val attachmentDownloadHandler = AttachmentDownloadHandler( + storage = storage, + messageDataProvider = messageDataProvider, + scope = viewModelScope, + ) + + @OptIn(FlowPreview::class) var timestamp: Long = 0L set(value) { job?.cancel() @@ -73,75 +88,93 @@ class MessageDetailsViewModel @Inject constructor( return } - val mmsRecord = messageRecord as? MmsMessageRecord - + // listen to conversation and attachments changes job = viewModelScope.launch { - repository.changes(messageRecord.threadId) - .filter { mmsSmsDatabase.getMessageForTimestamp(value) == null } - .collect { event.send(Event.Finish) } + (context.contentResolver.observeChanges(DatabaseContentProviders.Conversation.getUriForThread(messageRecord.threadId)) as Flow<*>) + .debounce(200L) + .onStart { emit(Unit) } + .collect{ + val updatedRecord = mmsSmsDatabase.getMessageForTimestamp(value) + if(updatedRecord == null) event.send(Event.Finish) + else { + createStateFromRecord(updatedRecord) + } + } } + } - viewModelScope.launch { - state.value = messageRecord.run { - val slides = mmsRecord?.slideDeck?.slides ?: emptyList() + private suspend fun createStateFromRecord(messageRecord: MessageRecord){ + val mmsRecord = messageRecord as? MmsMessageRecord - val conversation = threadDb.getRecipientForThreadId(threadId)!! - val isDeprecatedLegacyGroup = conversation.isLegacyGroupRecipient && - deprecationManager.isDeprecated + withContext(Dispatchers.Default){ + state.value = messageRecord.run { + val slides = mmsRecord?.slideDeck?.slides ?: emptyList() + val conversation = threadDb.getRecipientForThreadId(threadId)!! + val isDeprecatedLegacyGroup = conversation.isLegacyGroupRecipient && + deprecationManager.isDeprecated - val errorString = lokiMessageDatabase.getErrorMessage(id) - var status: MessageStatus? = null - // create a 'failed to send' status if appropriate - if(messageRecord.isFailed){ - status = MessageStatus( - title = context.getString(R.string.messageStatusFailedToSend), - icon = R.drawable.ic_triangle_alert, - errorStatus = true - ) - } + val errorString = lokiMessageDatabase.getErrorMessage(id) - val sender = if(messageRecord.isOutgoing){ - Recipient.from(context, Address.fromSerialized(prefs.getLocalNumber() ?: ""), false) - } else individualRecipient - - MessageDetailsState( - attachments = slides.map(::Attachment), - record = messageRecord, - - // Set the "Sent" message info TitledText appropriately - sent = if (messageRecord.isSending && errorString == null) { - val sendingWithEllipsisString = context.getString(R.string.sending) + ellipsis // e.g., "Sending…" - TitledText(sendingWithEllipsisString, null) - } else if (messageRecord.isSent && errorString == null) { - dateReceived.let(::Date).toString().let { TitledText(R.string.sent, it) } - } else { - null // Not sending or sent? Don't display anything for the "Sent" element. - }, - - // Set the "Received" message info TitledText appropriately - received = if (messageRecord.isIncoming && errorString == null) { - dateReceived.let(::Date).toString().let { TitledText(R.string.received, it) } - } else { - null // Not incoming? Then don't display anything for the "Received" element. - }, - - error = errorString?.let { TitledText(context.getString(R.string.theError) + ":", it) }, - status = status, - senderInfo = sender.run { - TitledText( - if(messageRecord.isOutgoing) context.getString(R.string.you) else name, - address.toString() - ) - }, - sender = sender, - thread = conversation, - readOnly = isDeprecatedLegacyGroup + var status: MessageStatus? = null + // create a 'failed to send' status if appropriate + if(messageRecord.isFailed){ + status = MessageStatus( + title = context.getString(R.string.messageStatusFailedToSend), + icon = R.drawable.ic_triangle_alert, + errorStatus = true ) } + + val sender = if(messageRecord.isOutgoing){ + Recipient.from(context, Address.fromSerialized(prefs.getLocalNumber() ?: ""), false) + } else individualRecipient + + val attachments = slides.map(::Attachment) + + // we don't want to display image attachments in the carousel if their state isn't done + val imageAttachments = attachments.filter { it.isDownloaded && it.hasImage } + + MessageDetailsState( + //todo: ATTACHMENT We should sort out the equals in DatabaseAttachment which is the reason the StateFlow think the objects are the same in spite of the transferState of an attachment being different. That way we could remove the timestamp below + timestamp = System.currentTimeMillis(), // used as a trick to force the state as being marked aas different each time + attachments = attachments, + imageAttachments = imageAttachments, + record = messageRecord, + + // Set the "Sent" message info TitledText appropriately + sent = if (messageRecord.isSending && errorString == null) { + val sendingWithEllipsisString = context.getString(R.string.sending) + ellipsis // e.g., "Sending…" + TitledText(sendingWithEllipsisString, null) + } else if (messageRecord.isSent && errorString == null) { + dateReceived.let(::Date).toString().let { TitledText(R.string.sent, it) } + } else { + null // Not sending or sent? Don't display anything for the "Sent" element. + }, + + // Set the "Received" message info TitledText appropriately + received = if (messageRecord.isIncoming && errorString == null) { + dateReceived.let(::Date).toString().let { TitledText(R.string.received, it) } + } else { + null // Not incoming? Then don't display anything for the "Received" element. + }, + + error = errorString?.let { TitledText(context.getString(R.string.theError) + ":", it) }, + status = status, + senderInfo = sender.run { + TitledText( + if(messageRecord.isOutgoing) context.getString(R.string.you) else name, + address.toString() + ) + }, + sender = sender, + thread = conversation, + readOnly = isDeprecatedLegacyGroup + ) } } + } private val Slide.details: List get() = listOfNotNull( @@ -162,27 +195,27 @@ class MessageDetailsViewModel @Inject constructor( ?.takeIf { it > 0 } ?.let { String.format( + Locale.getDefault(), "%01d:%02d", TimeUnit.MILLISECONDS.toMinutes(it), TimeUnit.MILLISECONDS.toSeconds(it) % 60 ) } - fun Attachment(slide: Slide): Attachment = Attachment(slide.details, slide.filename, slide.uri, hasImage = (slide is ImageSlide)) + fun Attachment(slide: Slide): Attachment = Attachment( + fileDetails = slide.details, + fileName = slide.filename, + uri = slide.thumbnailUri, + hasImage = slide.hasImage(), + isDownloaded = slide.isDone + ) fun onClickImage(index: Int) { val state = state.value val mmsRecord = state.mmsRecord ?: return val slide = mmsRecord.slideDeck.slides[index] ?: return // only open to downloaded images - if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) { - // Restart download here (on IO thread) - (slide.asAttachment() as? DatabaseAttachment)?.let { attachment -> - onAttachmentNeedsDownload(attachment) - } - } - - if (slide.isInProgress) return + if (slide.isInProgress || slide.isFailed) return viewModelScope.launch { MediaPreviewArgs(slide, state.mmsRecord, state.thread) @@ -191,16 +224,15 @@ class MessageDetailsViewModel @Inject constructor( } } - fun onAttachmentNeedsDownload(attachment: DatabaseAttachment) { - viewModelScope.launch(Dispatchers.IO) { - JobQueue.shared.add(AttachmentDownloadJob(attachment.attachmentId.rowId, attachment.mmsId)) - } + fun retryFailedAttachments(attachments: List){ + attachmentDownloadHandler.retryFailedAttachments(attachments) } } data class MessageDetailsState( + val timestamp: Long = 0L, val attachments: List = emptyList(), - val imageAttachments: List = attachments.filter { it.hasImage }, + val imageAttachments: List = emptyList(), val nonImageAttachmentFileDetails: List? = attachments.firstOrNull { !it.hasImage }?.fileDetails, val record: MessageRecord? = null, val mmsRecord: MmsMessageRecord? = record as? MmsMessageRecord, @@ -222,7 +254,8 @@ data class Attachment( val fileDetails: List, val fileName: String?, val uri: Uri?, - val hasImage: Boolean + val hasImage: Boolean, + val isDownloaded: Boolean ) data class MessageStatus( diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt index 0b61dec9d44..063d0bbf445 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt @@ -11,10 +11,10 @@ import android.widget.RelativeLayout import android.widget.TextView import androidx.core.view.children import androidx.core.view.isVisible +import com.bumptech.glide.RequestManager import com.squareup.phrase.Phrase import network.loki.messenger.R import network.loki.messenger.databinding.AlbumThumbnailViewBinding -import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY import org.session.libsession.utilities.recipients.Recipient @@ -22,7 +22,6 @@ import org.thoughtcrime.securesms.MediaPreviewActivity import org.thoughtcrime.securesms.components.CornerMask import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView import org.thoughtcrime.securesms.database.model.MmsMessageRecord -import com.bumptech.glide.RequestManager import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.util.ActivityDispatcher @@ -50,7 +49,7 @@ class AlbumThumbnailView : RelativeLayout { // region Interaction - fun calculateHitObject(event: MotionEvent, mms: MmsMessageRecord, threadRecipient: Recipient, onAttachmentNeedsDownload: (DatabaseAttachment) -> Unit) { + fun calculateHitObject(event: MotionEvent, mms: MmsMessageRecord, threadRecipient: Recipient, downloadPendingAttachment: (DatabaseAttachment) -> Unit) { val rawXInt = event.rawX.toInt() val rawYInt = event.rawY.toInt() val eventRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt) @@ -62,10 +61,10 @@ class AlbumThumbnailView : RelativeLayout { // hit intersects with this particular child val slide = slides.getOrNull(index) ?: return@forEach // only open to downloaded images - if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) { + if (slide.isFailed) { // Restart download here (on IO thread) (slide.asAttachment() as? DatabaseAttachment)?.let { attachment -> - onAttachmentNeedsDownload(attachment) + downloadPendingAttachment(attachment) } } if (slide.isInProgress) return@forEach diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/AttachmentControlView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/AttachmentControlView.kt new file mode 100644 index 00000000000..9e053399eee --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/AttachmentControlView.kt @@ -0,0 +1,175 @@ +package org.thoughtcrime.securesms.conversation.v2.messages + +import android.content.Context +import android.content.res.ColorStateList +import android.util.AttributeSet +import android.widget.LinearLayout +import androidx.annotation.ColorInt +import androidx.core.view.isVisible +import com.squareup.phrase.Phrase +import dagger.hilt.android.AndroidEntryPoint +import network.loki.messenger.R +import network.loki.messenger.databinding.ViewAttachmentControlBinding +import org.session.libsession.database.StorageProtocol +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState +import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment +import org.session.libsession.utilities.StringSubstitutionConstants.FILE_TYPE_KEY +import org.session.libsession.utilities.Util +import org.session.libsession.utilities.getColorFromAttr +import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.conversation.v2.dialogs.AutoDownloadDialog +import org.thoughtcrime.securesms.mms.Slide +import org.thoughtcrime.securesms.ui.findActivity +import org.thoughtcrime.securesms.util.ActivityDispatcher +import java.util.Locale +import javax.inject.Inject + +@AndroidEntryPoint +class AttachmentControlView: LinearLayout { + private val binding by lazy { ViewAttachmentControlBinding.bind(this) } + enum class AttachmentType { + VOICE, + AUDIO, + DOCUMENT, + IMAGE, + VIDEO, + } + + // region Lifecycle + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + // endregion + @Inject lateinit var storage: StorageProtocol + + val errorColor by lazy { + context.getColorFromAttr(R.attr.danger) + } + + // region Updating + private fun getAttachmentData(attachmentType: AttachmentType, messageTotalAttachment: Int): Pair { + return when (attachmentType) { + AttachmentType.VOICE -> Pair(R.string.messageVoice, R.drawable.ic_mic) + AttachmentType.AUDIO -> Pair(R.string.audio, R.drawable.ic_volume_2) + AttachmentType.DOCUMENT -> Pair(R.string.document, R.drawable.ic_file) + AttachmentType.IMAGE -> { + if(messageTotalAttachment > 1) Pair(R.string.images, R.drawable.ic_images) + else Pair(R.string.image, R.drawable.ic_image) + } + AttachmentType.VIDEO -> Pair(R.string.video, R.drawable.ic_square_play) + } + } + + fun bind( + attachmentType: AttachmentType, + @ColorInt textColor: Int, + state: AttachmentState, + allMessageAttachments: List, + ) { + val (stringRes, iconRes) = getAttachmentData(attachmentType, allMessageAttachments.size) + + val totalSize = Util.getPrettyFileSize(allMessageAttachments.sumOf { it.fileSize }) + + binding.pendingDownloadIcon.setImageResource(iconRes) + + when(state){ + AttachmentState.EXPIRED -> { + val expiredColor = textColor.also { alpha = 0.7f } + + binding.pendingDownloadIcon.setColorFilter(expiredColor) + binding.pendingDownloadSize.isVisible = false + + binding.pendingDownloadTitle.apply { + text = context.getString(R.string.attachmentsExpired) + setTextColor(expiredColor) + setTypeface(typeface, android.graphics.Typeface.ITALIC) + } + + binding.separator.isVisible = false + binding.pendingDownloadSubtitle.isVisible = false + } + + AttachmentState.DOWNLOADING -> { + binding.pendingDownloadIcon.setColorFilter(textColor) + + //todo: ATTACHMENT This will need to be tweaked to dynamically show the the downloaded amount + binding.pendingDownloadSize.apply { + text = totalSize + setTextColor(textColor) + isVisible = true + } + + binding.pendingDownloadTitle.apply{ + text = context.getString(R.string.downloading) + setTextColor(textColor) + setTypeface(typeface, android.graphics.Typeface.NORMAL) + } + + binding.separator.apply { + imageTintList = ColorStateList.valueOf(textColor) + isVisible = true + } + + binding.pendingDownloadSubtitle.isVisible = false + } + + AttachmentState.FAILED -> { + binding.pendingDownloadIcon.setColorFilter(textColor) + + binding.pendingDownloadSize.apply { + text = totalSize + setTextColor(errorColor) + isVisible = true + } + + binding.pendingDownloadTitle.apply{ + text = context.getString(R.string.failedToDownload) + setTextColor(errorColor) + setTypeface(typeface, android.graphics.Typeface.NORMAL) + } + + binding.separator.apply { + imageTintList = ColorStateList.valueOf(errorColor) + isVisible = true + } + + binding.pendingDownloadSubtitle.isVisible = true + } + + else -> { + binding.pendingDownloadIcon.setColorFilter(textColor) + + binding.pendingDownloadSize.apply { + text = totalSize + setTextColor(textColor) + isVisible = true + } + + binding.pendingDownloadTitle.apply{ + text = Phrase.from(context, R.string.attachmentsTapToDownload) + .put(FILE_TYPE_KEY, context.getString(stringRes).lowercase(Locale.ROOT)) + .format() + setTextColor(textColor) + setTypeface(typeface, android.graphics.Typeface.NORMAL) + } + + binding.separator.apply { + imageTintList = ColorStateList.valueOf(textColor) + isVisible = true + } + + binding.pendingDownloadSubtitle.isVisible = false + } + } + } + // endregion + + // region Interaction + fun showDownloadDialog(threadRecipient: Recipient, attachment: DatabaseAttachment) { + if (!storage.shouldAutoDownloadAttachments(threadRecipient)) { + // just download + (context.findActivity() as? ActivityDispatcher)?.showDialog(AutoDownloadDialog(threadRecipient, attachment)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DeletedMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DeletedMessageView.kt index 7a495a9478a..737d69de6b2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DeletedMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DeletedMessageView.kt @@ -23,8 +23,9 @@ class DeletedMessageView : LinearLayout { assert(message.isDeleted) // set the text to the message's body if it is set, else use a fallback binding.deleteTitleTextView.text = message.body.ifEmpty { context.resources.getQuantityString(R.plurals.deleteMessageDeleted, 1, 1) } - binding.deleteTitleTextView.setTextColor(textColor) - binding.deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor) + val deletedColor = textColor.also { alpha = 0.7f } // deleted messages use the regular text colour with some opacitiy applied) + binding.deleteTitleTextView.setTextColor(deletedColor) + binding.deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(deletedColor) } // endregion } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt index 2b0c091f7e6..b14e60f6ecf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt @@ -7,6 +7,7 @@ import android.widget.LinearLayout import androidx.annotation.ColorInt import androidx.core.view.isVisible import network.loki.messenger.databinding.ViewDocumentBinding +import org.session.libsession.utilities.Util import org.thoughtcrime.securesms.database.model.MmsMessageRecord class DocumentView : LinearLayout { @@ -23,6 +24,8 @@ class DocumentView : LinearLayout { val document = message.slideDeck.documentSlide!! binding.documentTitleTextView.text = document.filename binding.documentTitleTextView.setTextColor(textColor) + binding.documentSize.text = Util.getPrettyFileSize(document.fileSize) + binding.documentSize.setTextColor(textColor) binding.documentViewIconImageView.imageTintList = ColorStateList.valueOf(textColor) // Show the progress spinner if the attachment is downloading, otherwise show diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt index d064d028724..b8a43b46904 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt @@ -42,7 +42,6 @@ class LinkPreviewView : LinearLayout { if (linkPreview.getThumbnail().isPresent) { // This internally fetches the thumbnail binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false) - binding.thumbnailImageView.root.loadIndicator.isVisible = false } // Title binding.titleTextView.text = linkPreview.title diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/PendingAttachmentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/PendingAttachmentView.kt deleted file mode 100644 index 72e37b5dddb..00000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/PendingAttachmentView.kt +++ /dev/null @@ -1,65 +0,0 @@ -package org.thoughtcrime.securesms.conversation.v2.messages - -import android.content.Context -import android.util.AttributeSet -import android.widget.LinearLayout -import androidx.annotation.ColorInt -import com.squareup.phrase.Phrase -import dagger.hilt.android.AndroidEntryPoint -import network.loki.messenger.R -import network.loki.messenger.databinding.ViewPendingAttachmentBinding -import org.session.libsession.database.StorageProtocol -import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment -import org.session.libsession.utilities.StringSubstitutionConstants.FILE_TYPE_KEY -import org.session.libsession.utilities.recipients.Recipient -import org.thoughtcrime.securesms.conversation.v2.dialogs.AutoDownloadDialog -import org.thoughtcrime.securesms.util.ActivityDispatcher -import org.thoughtcrime.securesms.util.displaySize -import java.util.Locale -import javax.inject.Inject - -@AndroidEntryPoint -class PendingAttachmentView: LinearLayout { - private val binding by lazy { ViewPendingAttachmentBinding.bind(this) } - enum class AttachmentType { - AUDIO, - DOCUMENT, - IMAGE, - VIDEO, - } - - // region Lifecycle - constructor(context: Context) : super(context) - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) - - // endregion - @Inject lateinit var storage: StorageProtocol - - // region Updating - fun bind(attachmentType: AttachmentType, @ColorInt textColor: Int, attachment: DatabaseAttachment) { - val stringRes = when (attachmentType) { - AttachmentType.AUDIO -> R.string.audio - AttachmentType.DOCUMENT -> R.string.document - AttachmentType.IMAGE -> R.string.image - AttachmentType.VIDEO -> R.string.video - } - - val text = Phrase.from(context, R.string.attachmentsTapToDownload) - .put(FILE_TYPE_KEY, context.getString(stringRes).lowercase(Locale.ROOT)) - .format() - - binding.pendingDownloadIcon.setColorFilter(textColor) - binding.pendingDownloadSize.text = attachment.displaySize() - binding.pendingDownloadTitle.text = text - } - // endregion - - // region Interaction - fun showDownloadDialog(threadRecipient: Recipient, attachment: DatabaseAttachment) { - if (!storage.shouldAutoDownloadAttachments(threadRecipient)) { - // just download - ActivityDispatcher.get(context)?.showDialog(AutoDownloadDialog(threadRecipient, attachment)) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt index 4b572a1a3f1..bd3a88c9730 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt @@ -100,33 +100,47 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet? binding.quoteViewAccentLine.setBackgroundColor(getLineColor(isOutgoingMessage)) } else if (attachments != null) { binding.quoteViewAttachmentPreviewImageView.imageTintList = ColorStateList.valueOf(textColor) - binding.quoteViewAttachmentPreviewImageView.isVisible = false + binding.quoteViewAttachmentPreviewImageView.isVisible = true binding.quoteViewAttachmentThumbnailImageView.root.isVisible = false when { attachments.audioSlide != null -> { - binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_mic) - binding.quoteViewAttachmentPreviewImageView.isVisible = true - val isVoiceNote = attachments.isVoiceNote - binding.quoteViewBodyTextView.text = if (isVoiceNote) { - resources.getString(R.string.messageVoice) + if (isVoiceNote) { + binding.quoteViewBodyTextView.text = resources.getString(R.string.messageVoice) + binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_mic) } else { - resources.getString(R.string.audio) + binding.quoteViewBodyTextView.text = resources.getString(R.string.audio) + binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_volume_2) } } attachments.documentSlide != null -> { binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_file) - binding.quoteViewAttachmentPreviewImageView.isVisible = true binding.quoteViewBodyTextView.text = resources.getString(R.string.document) } attachments.thumbnailSlide != null -> { val slide = attachments.thumbnailSlide!! - // This internally fetches the thumbnail - binding.quoteViewAttachmentThumbnailImageView - .root.setRoundedCorners(toPx(4, resources)) - binding.quoteViewAttachmentThumbnailImageView.root.setImageResource(glide, slide, false) - binding.quoteViewAttachmentThumbnailImageView.root.isVisible = true - binding.quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.video) else resources.getString(R.string.image) + + if (MediaUtil.isVideo(slide.asAttachment())){ + binding.quoteViewBodyTextView.text = resources.getString(R.string.video) + binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_square_play) + } else { + binding.quoteViewBodyTextView.text = resources.getString(R.string.image) + binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_image) + } + + // display the image if we are in the appropriate state + if(attachments.asAttachments().all { it.isDone }) { + binding.quoteViewAttachmentThumbnailImageView + .root.setRoundedCorners(toPx(4, resources)) + binding.quoteViewAttachmentThumbnailImageView.root.setImageResource( + glide, + slide, + false + ) + binding.quoteViewAttachmentThumbnailImageView.root.isVisible = true + binding.quoteViewAttachmentPreviewImageView.isVisible = false + } + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index 3f336a9be11..61d81b037ce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -27,13 +27,20 @@ import com.bumptech.glide.RequestManager import network.loki.messenger.R import network.loki.messenger.databinding.ViewVisibleMessageContentBinding import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress +import org.session.libsession.messaging.jobs.AttachmentDownloadJob +import org.session.libsession.messaging.jobs.JobQueue +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.getColorFromAttr import org.session.libsession.utilities.modifyLayoutParams import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 +import org.thoughtcrime.securesms.conversation.v2.messages.AttachmentControlView.AttachmentType.AUDIO +import org.thoughtcrime.securesms.conversation.v2.messages.AttachmentControlView.AttachmentType.DOCUMENT +import org.thoughtcrime.securesms.conversation.v2.messages.AttachmentControlView.AttachmentType.IMAGE +import org.thoughtcrime.securesms.conversation.v2.messages.AttachmentControlView.AttachmentType.VIDEO +import org.thoughtcrime.securesms.conversation.v2.messages.AttachmentControlView.AttachmentType.VOICE import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans @@ -66,7 +73,8 @@ class VisibleMessageContentView : ConstraintLayout { glide: RequestManager = Glide.with(this), thread: Recipient, searchQuery: String? = null, - onAttachmentNeedsDownload: (DatabaseAttachment) -> Unit, + downloadPendingAttachment: (DatabaseAttachment) -> Unit, + retryFailedAttachments: (List) -> Unit, suppressThumbnails: Boolean = false ) { // Background @@ -75,9 +83,19 @@ class VisibleMessageContentView : ConstraintLayout { binding.contentParent.mainColor = color binding.contentParent.cornerRadius = resources.getDimension(R.dimen.message_corner_radius) - val mediaDownloaded = message is MmsMessageRecord && message.slideDeck.asAttachments().all { it.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE } + val mediaDownloaded = message is MmsMessageRecord && message.slideDeck.asAttachments().all { it.isDone } val mediaInProgress = message is MmsMessageRecord && message.slideDeck.asAttachments().any { it.isInProgress } - val mediaThumbnailMessage = message is MmsMessageRecord && message.slideDeck.thumbnailSlide != null + val hasFailed = message is MmsMessageRecord && message.slideDeck.asAttachments().any { it.isFailed } + val hasExpired = haveAttachmentsExpired(message) + val overallAttachmentState = when { + mediaDownloaded -> AttachmentState.DONE + hasExpired -> AttachmentState.EXPIRED + hasFailed -> AttachmentState.FAILED + mediaInProgress -> AttachmentState.DOWNLOADING + else -> AttachmentState.PENDING + } + + val databaseAttachments = (message as? MmsMessageRecord)?.slideDeck?.asAttachments()?.filterIsInstance() // reset visibilities / containers onContentClick.clear() @@ -104,11 +122,19 @@ class VisibleMessageContentView : ConstraintLayout { // sized based on text content from a recycled view binding.bodyTextView.text = null binding.quoteView.root.isVisible = message is MmsMessageRecord && message.quote != null + // if a quote is by itself we should add bottom padding + binding.quoteView.root.setPadding( + binding.quoteView.root.paddingStart, + binding.quoteView.root.paddingTop, + binding.quoteView.root.paddingEnd, + if(message.body.isNotEmpty()) 0 else + context.resources.getDimensionPixelSize(R.dimen.message_spacing) + ) binding.linkPreviewView.root.isVisible = message is MmsMessageRecord && message.linkPreviews.isNotEmpty() - binding.pendingAttachmentView.root.isVisible = !mediaDownloaded && !mediaInProgress && message is MmsMessageRecord && message.quote == null && message.linkPreviews.isEmpty() - binding.voiceMessageView.root.isVisible = (mediaDownloaded || mediaInProgress) && message is MmsMessageRecord && message.slideDeck.audioSlide != null - binding.documentView.root.isVisible = (mediaDownloaded || mediaInProgress) && message is MmsMessageRecord && message.slideDeck.documentSlide != null - binding.albumThumbnailView.root.isVisible = mediaThumbnailMessage + binding.attachmentControlView.root.isVisible = false + binding.voiceMessageView.root.isVisible = false + binding.documentView.root.isVisible = false + binding.albumThumbnailView.root.isVisible = false binding.openGroupInvitationView.root.isVisible = message.isOpenGroupInvitation var hideBody = false @@ -134,13 +160,12 @@ class VisibleMessageContentView : ConstraintLayout { } if (message is MmsMessageRecord) { - message.slideDeck.asAttachments().forEach { attach -> - val dbAttachment = attach as? DatabaseAttachment ?: return@forEach - onAttachmentNeedsDownload(dbAttachment) + databaseAttachments?.forEach { attach -> + downloadPendingAttachment(attach) } message.linkPreviews.forEach { preview -> val previewThumbnail = preview.getThumbnail().orNull() as? DatabaseAttachment ?: return@forEach - onAttachmentNeedsDownload(previewThumbnail) + downloadPendingAttachment(previewThumbnail) } } @@ -161,7 +186,8 @@ class VisibleMessageContentView : ConstraintLayout { hideBody = false // Audio attachment - if (mediaDownloaded || mediaInProgress || message.isOutgoing) { + if (overallAttachmentState == AttachmentState.DONE || message.isOutgoing) { + binding.voiceMessageView.root.isVisible = true binding.voiceMessageView.root.indexInAdapter = indexInAdapter binding.voiceMessageView.root.delegate = context as? ConversationActivityV2 binding.voiceMessageView.root.bind(message, isStartOfMessageCluster, isEndOfMessageCluster) @@ -169,27 +195,37 @@ class VisibleMessageContentView : ConstraintLayout { // message view) so as to not interfere with all the other gestures. onContentClick.add { binding.voiceMessageView.root.togglePlayback() } onContentDoubleTap = { binding.voiceMessageView.root.handleDoubleTap() } + binding.attachmentControlView.root.isVisible = false } else { - // If it's an audio message but we haven't downloaded it yet show it as pending - (message.slideDeck.audioSlide?.asAttachment() as? DatabaseAttachment)?.let { attachment -> - binding.pendingAttachmentView.root.bind( - PendingAttachmentView.AttachmentType.AUDIO, - getTextColor(context,message), - attachment + val attachment = message.slideDeck.audioSlide?.asAttachment() as? DatabaseAttachment + attachment?.let { + showAttachmentControl( + thread = thread, + message = message, + attachments = listOf(it), + type = if (it.isVoiceNote) VOICE + else AUDIO, + overallAttachmentState, + retryFailedAttachments = retryFailedAttachments ) - onContentClick.add { binding.pendingAttachmentView.root.showDownloadDialog(thread, attachment) } } } } + //todo: ATTACHMENT should the glowView encompass the whole message instead of just the body? Currently tapped quotes only highlight text messages, not images nor attachment control + // DOCUMENT message is MmsMessageRecord && message.slideDeck.documentSlide != null -> { // Show any message that came with the attached document hideBody = false - + // Document attachment - if (mediaDownloaded || mediaInProgress || message.isOutgoing) { + if (overallAttachmentState == AttachmentState.DONE || message.isOutgoing) { + binding.attachmentControlView.root.isVisible = false + + binding.documentView.root.isVisible = true binding.documentView.root.bind(message, getTextColor(context, message)) + message.slideDeck.documentSlide?.let { slide -> if(!mediaInProgress) { // do not attempt to open a doc in progress of downloading onContentClick.add { @@ -215,25 +251,31 @@ class VisibleMessageContentView : ConstraintLayout { } } } else { - // If the document hasn't been downloaded yet then show it as pending - (message.slideDeck.documentSlide?.asAttachment() as? DatabaseAttachment)?.let { attachment -> - binding.pendingAttachmentView.root.bind( - PendingAttachmentView.AttachmentType.DOCUMENT, - getTextColor(context,message), - attachment - ) - onContentClick.add { - binding.pendingAttachmentView.root.showDownloadDialog(thread, attachment) - } + (message.slideDeck.documentSlide?.asAttachment() as? DatabaseAttachment)?.let { + showAttachmentControl( + thread = thread, + message = message, + attachments = listOf(it), + type = DOCUMENT, + overallAttachmentState, + retryFailedAttachments = retryFailedAttachments + ) } } } // IMAGE / VIDEO - message is MmsMessageRecord && !suppressThumbnails && message.slideDeck.asAttachments().isNotEmpty() -> { - if (mediaDownloaded || mediaInProgress || message.isOutgoing) { + message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty() -> { + hideBody = false + + if (overallAttachmentState == AttachmentState.DONE || message.isOutgoing) { + if(suppressThumbnails) return // suppress thumbnail should hide the image, but we still want to show the attachment control if the state demands it + + binding.attachmentControlView.root.isVisible = false + // isStart and isEnd of cluster needed for calculating the mask for full bubble image groups // bind after add view because views are inflated and calculated during bind + binding.albumThumbnailView.root.isVisible = true binding.albumThumbnailView.root.bind( glideRequests = glide, message = message, @@ -244,21 +286,19 @@ class VisibleMessageContentView : ConstraintLayout { horizontalBias = if (message.isOutgoing) 1f else 0f } onContentClick.add { event -> - binding.albumThumbnailView.root.calculateHitObject(event, message, thread, onAttachmentNeedsDownload) + binding.albumThumbnailView.root.calculateHitObject(event, message, thread, downloadPendingAttachment) } } else { - hideBody = true - binding.albumThumbnailView.root.clearViews() - val firstAttachment = message.slideDeck.asAttachments().first() as? DatabaseAttachment - firstAttachment?.let { attachment -> - binding.pendingAttachmentView.root.bind( - PendingAttachmentView.AttachmentType.IMAGE, - getTextColor(context,message), - attachment - ) - onContentClick.add { - binding.pendingAttachmentView.root.showDownloadDialog(thread, attachment) - } + databaseAttachments?.let { + showAttachmentControl( + thread = thread, + message = message, + attachments = it, + type = if (message.slideDeck.hasVideo()) VIDEO + else IMAGE, + state = overallAttachmentState, + retryFailedAttachments = retryFailedAttachments + ) } } } @@ -287,8 +327,61 @@ class VisibleMessageContentView : ConstraintLayout { binding.contentParent.modifyLayoutParams { horizontalBias = if (message.isOutgoing) 1f else 0f } + + binding.attachmentControlView.root.modifyLayoutParams { + horizontalBias = if (message.isOutgoing) 1f else 0f + } } + private fun showAttachmentControl( + thread: Recipient, + message: MmsMessageRecord, + attachments: List, + type: AttachmentControlView.AttachmentType, + state: AttachmentState, + retryFailedAttachments: (List) -> Unit, + ){ + binding.attachmentControlView.root.isVisible = true + binding.albumThumbnailView.root.clearViews() + + binding.attachmentControlView.root.bind( + attachmentType = type, + textColor = getTextColor(context,message), + state = state, + allMessageAttachments = message.slideDeck.slides + ) + + when(state) { + // While downloads haven't been enabled for this convo, show a confirmation dialog + AttachmentState.PENDING -> { + onContentClick.add { + binding.attachmentControlView.root.showDownloadDialog( + thread, + attachments.first() + ) + } + } + + // Attempt to redownload a failed attachment on tap + AttachmentState.FAILED -> { + onContentClick.add { + retryFailedAttachments(attachments) + } + } + + // no click actions for other cases + else -> {} + } + } + + private fun haveAttachmentsExpired(message: MessageRecord): Boolean = + // expired attachments are for Mms records only + message is MmsMessageRecord && + // with a state marked as expired + (message.slideDeck.asAttachments().any { it.transferState == AttachmentState.EXPIRED.value } || + // with a state marked as downloaded yet without a URI attached + (!message.hasAttachmentUri() && message.slideDeck.asAttachments().all { it.isDone })) + private val onContentClick: MutableList<((event: MotionEvent) -> Unit)> = mutableListOf() fun onContentClick(event: MotionEvent) { @@ -301,7 +394,7 @@ class VisibleMessageContentView : ConstraintLayout { fun recycle() { arrayOf( binding.deletedMessageView.root, - binding.pendingAttachmentView.root, + binding.attachmentControlView.root, binding.voiceMessageView.root, binding.openGroupInvitationView.root, binding.documentView.root, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index 8ab0baa3d18..1f1868dfeef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -167,7 +167,8 @@ class VisibleMessageView : FrameLayout { senderAccountID: String, lastSeen: Long, delegate: VisibleMessageViewDelegate? = null, - onAttachmentNeedsDownload: (DatabaseAttachment) -> Unit + downloadPendingAttachment: (DatabaseAttachment) -> Unit, + retryFailedAttachments: (List) -> Unit, ) { clipToPadding = false clipChildren = false @@ -300,7 +301,8 @@ class VisibleMessageView : FrameLayout { glide, thread, searchQuery, - onAttachmentNeedsDownload + downloadPendingAttachment = downloadPendingAttachment, + retryFailedAttachments = retryFailedAttachments ) binding.messageContentView.root.delegate = delegate onDoubleTap = { binding.messageContentView.root.onContentDoubleTap?.invoke() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt index 45ac96dc858..8a5b894a01e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt @@ -19,7 +19,6 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestOptions import network.loki.messenger.R import network.loki.messenger.databinding.ThumbnailViewBinding -import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress import org.session.libsession.utilities.Util.equals import org.session.libsession.utilities.getColorFromAttr import org.session.libsignal.utilities.ListenableFuture @@ -46,8 +45,6 @@ open class ThumbnailView @JvmOverloads constructor( // region Lifecycle - val loadIndicator: View by lazy { binding.thumbnailLoadIndicator } - private val dimensDelegate = ThumbnailDimensDelegate() private var slide: Slide? = null @@ -123,7 +120,7 @@ open class ThumbnailView @JvmOverloads constructor( naturalHeight: Int ): ListenableFuture { val showPlayOverlay = (slide.thumbnailUri != null && slide.hasPlayOverlay() && - (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview)) + (slide.isDone || isPreview)) if(showPlayOverlay) { binding.playOverlay.isVisible = true // The views are poorly constructed at the moment and there is no good way to know @@ -147,10 +144,6 @@ open class ThumbnailView @JvmOverloads constructor( this.slide = slide - binding.thumbnailLoadIndicator.isVisible = slide.isInProgress - binding.thumbnailDownloadIcon.isVisible = - slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED - dimensDelegate.setDimens(naturalWidth, naturalHeight) invalidate() @@ -158,7 +151,7 @@ open class ThumbnailView @JvmOverloads constructor( when { slide.thumbnailUri != null -> { buildThumbnailGlideRequest(glide, slide).into( - GlideDrawableListeningTarget(binding.thumbnailImage, binding.thumbnailLoadIndicator, it) + GlideDrawableListeningTarget(binding.thumbnailImage, null, it) ) } slide.hasPlaceholder() -> { @@ -208,7 +201,7 @@ open class ThumbnailView @JvmOverloads constructor( private fun RequestBuilder.intoDrawableTargetAsFuture() = SettableFuture().also { binding.run { - GlideDrawableListeningTarget(thumbnailImage, thumbnailLoadIndicator, it) + GlideDrawableListeningTarget(thumbnailImage, null, it) }.let { into(it) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java index 52d4e03a804..5299885f7a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -25,34 +25,21 @@ import android.net.Uri; import android.text.TextUtils; import android.util.Pair; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; + import com.bumptech.glide.Glide; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import kotlin.jvm.Synchronized; + import net.zetetic.database.sqlcipher.SQLiteDatabase; + import org.apache.commons.lang3.StringUtils; import org.json.JSONArray; import org.json.JSONException; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId; -import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState; import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment; import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachmentAudioExtras; import org.session.libsession.utilities.MediaTypes; @@ -72,10 +59,29 @@ import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.util.BitmapUtil; -import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData; import org.thoughtcrime.securesms.util.MediaUtil; +import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData; import org.thoughtcrime.securesms.video.EncryptedMediaDataSource; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; + +import kotlin.jvm.Synchronized; + public class AttachmentDatabase extends Database { private static final String TAG = AttachmentDatabase.class.getSimpleName(); @@ -194,7 +200,7 @@ public void setTransferProgressFailed(AttachmentId attachmentId, long mmsId) { SQLiteDatabase database = databaseHelper.getWritableDatabase(); ContentValues values = new ContentValues(); - values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED); + values.put(TRANSFER_STATE, AttachmentState.FAILED.getValue()); database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()); notifyConversationListeners(DatabaseComponent.get(context).mmsDatabase().getThreadIdForMessage(mmsId)); @@ -253,7 +259,7 @@ public void setTransferProgressFailed(AttachmentId attachmentId, long mmsId) Cursor cursor = null; try { - cursor = database.query(TABLE_NAME, PROJECTION, TRANSFER_STATE + " = ?", new String[] {String.valueOf(AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED)}, null, null, null); + cursor = database.query(TABLE_NAME, PROJECTION, TRANSFER_STATE + " = ?", new String[] {String.valueOf(AttachmentState.DOWNLOADING.getValue())}, null, null, null); while (cursor != null && cursor.moveToNext()) { attachments.addAll(getAttachment(cursor)); } @@ -264,6 +270,30 @@ public void setTransferProgressFailed(AttachmentId attachmentId, long mmsId) return attachments; } + public @NonNull List getAllAttachments() { + SQLiteDatabase database = databaseHelper.getReadableDatabase(); + Cursor cursor = null; + List attachments = new ArrayList<>(); + + try { + // Query all rows in the attachment table. + cursor = database.query(TABLE_NAME, PROJECTION, null, null, null, null, null); + + while (cursor != null && cursor.moveToNext()) { + List list = getAttachment(cursor); + if (list != null && !list.isEmpty()) { + attachments.addAll(list); + } + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + + return attachments; + } + void deleteAttachmentsForMessages(String[] messageIds) { StringBuilder queryBuilder = new StringBuilder(); for (int i = 0; i < messageIds.length; i++) { @@ -430,7 +460,7 @@ public void insertAttachmentsForPlaceholder(long mmsId, @NonNull AttachmentId at values.put(DATA_RANDOM, dataInfo.random); } - values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_DONE); + values.put(TRANSFER_STATE, AttachmentState.DONE.getValue()); values.put(CONTENT_LOCATION, (String)null); values.put(CONTENT_DISPOSITION, (String)null); values.put(DIGEST, (byte[])null); @@ -453,7 +483,7 @@ public void updateAttachmentAfterUploadSucceeded(@NonNull AttachmentId id, @NonN SQLiteDatabase database = databaseHelper.getWritableDatabase(); ContentValues values = new ContentValues(); - values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_DONE); + values.put(TRANSFER_STATE, AttachmentState.DONE.getValue()); values.put(CONTENT_LOCATION, attachment.getLocation()); values.put(DIGEST, attachment.getDigest()); values.put(CONTENT_DISPOSITION, attachment.getKey()); @@ -469,7 +499,7 @@ public void handleFailedAttachmentUpload(@NonNull AttachmentId id) { SQLiteDatabase database = databaseHelper.getWritableDatabase(); ContentValues values = new ContentValues(); - values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED); + values.put(TRANSFER_STATE, AttachmentState.FAILED.getValue()); database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings()); } @@ -568,7 +598,7 @@ public void markAttachmentUploaded(long messageId, Attachment attachment) { ContentValues values = new ContentValues(1); SQLiteDatabase database = databaseHelper.getWritableDatabase(); - values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_DONE); + values.put(TRANSFER_STATE, AttachmentState.DONE.getValue()); database.update(TABLE_NAME, values, PART_ID_WHERE, ((DatabaseAttachment)attachment).getAttachmentId().toStrings()); notifyConversationListeners(DatabaseComponent.get(context).mmsDatabase().getThreadIdForMessage(messageId)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java index 9f34f3fa0eb..430aeb39115 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java @@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; @@ -69,4 +70,17 @@ public boolean containsMediaSlide() { public @NonNull List getLinkPreviews() { return linkPreviews; } + + public boolean hasAttachmentUri() { + boolean hasData = false; + + for (Slide slide : slideDeck.getSlides()) { + if (slide.getUri() != null || slide.getThumbnailUri() != null) { + hasData = true; + break; + } + } + + return hasData; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt index f45c8ebbad7..02dd353cb8d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt @@ -39,6 +39,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview import network.loki.messenger.BuildConfig @@ -52,6 +53,7 @@ import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.LoadingDialog import org.thoughtcrime.securesms.ui.components.BackAppBar import org.thoughtcrime.securesms.ui.components.DropDown +import org.thoughtcrime.securesms.ui.components.SlimOutlineButton import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType @@ -157,6 +159,7 @@ fun DebugMenu( Column( modifier = Modifier .padding(horizontal = LocalDimensions.current.spacing) + .padding(bottom = LocalDimensions.current.spacing) .verticalScroll(rememberScrollState()) ) { // Info pane @@ -209,6 +212,12 @@ fun DebugMenu( sendCommand(DebugMenuViewModel.Commands.HideNoteToSelf(it)) } ) + + SlimOutlineButton( + "Clear All Trusted Downloads", + ) { + sendCommand(ClearTrustedDownloads) + } } // Group deprecation state diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt index 0f5484dd743..690e3834900 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt @@ -1,8 +1,11 @@ package org.thoughtcrime.securesms.debugmenu import android.app.Application +import android.content.Context +import android.widget.Toast import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -13,20 +16,27 @@ import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISI import org.session.libsession.utilities.Environment import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState +import org.thoughtcrime.securesms.database.AttachmentDatabase +import org.thoughtcrime.securesms.database.RecipientDatabase +import org.thoughtcrime.securesms.database.ThreadDatabase +import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.util.ClearDataUtils import java.time.ZonedDateTime import javax.inject.Inject @HiltViewModel class DebugMenuViewModel @Inject constructor( - private val application: Application, + @ApplicationContext private val context: Context, private val textSecurePreferences: TextSecurePreferences, private val configFactory: ConfigFactory, private val deprecationManager: LegacyGroupDeprecationManager, - private val clearDataUtils: ClearDataUtils + private val clearDataUtils: ClearDataUtils, + private val threadDb: ThreadDatabase, + private val recipientDatabase: RecipientDatabase, + private val attachmentDatabase: AttachmentDatabase, ) : ViewModel() { private val TAG = "DebugMenu" @@ -107,6 +117,10 @@ class DebugMenuViewModel @Inject constructor( is Commands.ShowDeprecationChangeDialog -> showDeprecatedStateWarningDialog(command.state) + + is Commands.ClearTrustedDownloads -> { + clearTrustedDownloads() + } } } @@ -157,6 +171,38 @@ class DebugMenuViewModel @Inject constructor( _uiState.value = _uiState.value.copy(showDeprecatedStateWarningDialog = true) } + private fun clearTrustedDownloads() { + // show a loading state + _uiState.value = _uiState.value.copy( + showEnvironmentWarningDialog = false, + showLoadingDialog = true + ) + + // clear trusted downloads for all recipients + viewModelScope.launch { + val conversations: List = threadDb.approvedConversationList.use { openCursor -> + threadDb.readerFor(openCursor).run { generateSequence { next }.toList() } + } + + conversations.filter { !it.recipient.isLocalNumber }.forEach { + recipientDatabase.setAutoDownloadAttachments(it.recipient, false) + } + + // set all attachments back to pending + attachmentDatabase.allAttachments.forEach { + attachmentDatabase.setTransferState(it.mmsId, it.attachmentId, AttachmentState.PENDING.value) + } + + Toast.makeText(context, "Cleared!", Toast.LENGTH_LONG).show() + + // hide loading + _uiState.value = _uiState.value.copy( + showEnvironmentWarningDialog = false, + showLoadingDialog = false + ) + } + } + data class UIState( val currentEnvironment: String, val environments: List, @@ -183,5 +229,6 @@ class DebugMenuViewModel @Inject constructor( object OverrideDeprecationState : Commands() data class OverrideDeprecatedTime(val time: ZonedDateTime) : Commands() data class OverrideDeprecatingStartTime(val time: ZonedDateTime) : Commands() + object ClearTrustedDownloads: Commands() } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/SeedReminder.kt b/app/src/main/java/org/thoughtcrime/securesms/home/SeedReminder.kt index 967681a1439..5e9f59fa2d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/SeedReminder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/SeedReminder.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import network.loki.messenger.R import org.thoughtcrime.securesms.ui.SessionShieldIcon import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton -import org.thoughtcrime.securesms.ui.components.SlimPrimaryOutlineButton import org.thoughtcrime.securesms.ui.contentDescription import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java index 6dcc928c996..84023436fb4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java @@ -11,7 +11,7 @@ import androidx.annotation.Nullable; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; -import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState; import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment; import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview; import org.session.libsession.utilities.MediaTypes; @@ -183,7 +183,7 @@ private static Optional bitmapToAttachment(@Nullable Bitmap bitmap, return Optional.of(new UriAttachment(uri, uri, contentType, - AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED, + AttachmentState.DOWNLOADING.getValue(), bytes.length, bitmap.getWidth(), bitmap.getHeight(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewScreen.kt index 4bdec3fdf52..2e4d4d49897 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewScreen.kt @@ -124,7 +124,7 @@ fun MediaOverviewScreen( topBar = { MediaOverviewTopAppBar( selectionMode = selectionMode, - title = viewModel.title.collectAsState().value, + title = stringResource(R.string.conversationsSettingsAllMedia), onBackClicked = viewModel::onBackClicked, onSaveClicked = { showingSaveAttachmentWarning = true }, onDeleteClicked = { showingDeleteConfirmation = true }, diff --git a/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewTopAppBar.kt b/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewTopAppBar.kt index 5332dd8da82..34476533ca2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewTopAppBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewTopAppBar.kt @@ -3,14 +3,18 @@ package org.thoughtcrime.securesms.media import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import network.loki.messenger.R import org.thoughtcrime.securesms.ui.components.ActionAppBar import org.thoughtcrime.securesms.ui.components.AppBarBackIcon import org.thoughtcrime.securesms.ui.theme.LocalColors +import org.thoughtcrime.securesms.ui.theme.PreviewTheme @Composable @OptIn(ExperimentalMaterial3Api::class) @@ -26,6 +30,7 @@ fun MediaOverviewTopAppBar( ) { ActionAppBar( title = title, + singleLine = true, actionModeTitle = numSelected.toString(), navigationIcon = { AppBarBackIcon(onBack = onBackClicked) }, scrollBehavior = appBarScrollBehavior, @@ -57,3 +62,23 @@ fun MediaOverviewTopAppBar( } ) } + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +fun PreviewMediaOverviewAppBar() { + PreviewTheme { + MediaOverviewTopAppBar( + selectionMode = false, + numSelected = 0, + title = "Really long title asdlkajsdlkasjdlaskdjalskdjaslkj", + onBackClicked = {}, + onSaveClicked = {}, + onDeleteClicked = {}, + onSelectAllClicked = {}, + appBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior( + rememberTopAppBarState() + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewViewModel.kt index 6c0822cc8b2..d1cb98bdd52 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewViewModel.kt @@ -64,10 +64,6 @@ class MediaOverviewViewModel( .map { Recipient.from(application, address, false) } .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) - val title: StateFlow = recipient - .map { it.name } - .stateIn(viewModelScope, SharingStarted.Eagerly, "") - val mediaListState: StateFlow = recipient .map { recipient -> withContext(Dispatchers.Default) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.kt b/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.kt index ab0e0986016..47fe00603e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.kt @@ -22,7 +22,7 @@ import android.net.Uri import androidx.annotation.DrawableRes import network.loki.messenger.R import org.session.libsession.messaging.sending_receiving.attachments.Attachment -import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment import org.session.libsession.utilities.MediaTypes import org.thoughtcrime.securesms.util.FilenameUtils @@ -58,7 +58,7 @@ class AudioSlide : Slide { uri, null, // thumbnailUri contentType, - AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED, + AttachmentState.DOWNLOADING.value, dataSize, 0, // width 0, // height @@ -72,7 +72,7 @@ class AudioSlide : Slide { constructor(context: Context, attachment: Attachment) : super(context, attachment) override fun hasPlaceholder() = true - override fun hasImage() = true + override fun hasImage() = false override fun hasAudio() = true // Legacy voice messages don't have filenames at all - so should we come across one we must synthesize a filename using the delivery date obtained from the attachment diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.kt b/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.kt index 961b4e11dbf..090663fdf90 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.kt @@ -19,13 +19,11 @@ package org.thoughtcrime.securesms.mms import android.content.Context import android.content.res.Resources import android.net.Uri -import android.util.Log import androidx.annotation.DrawableRes import com.squareup.phrase.Phrase -import kotlin.String import network.loki.messenger.R import org.session.libsession.messaging.sending_receiving.attachments.Attachment -import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY import org.session.libsession.utilities.Util.equals @@ -106,10 +104,19 @@ abstract class Slide(@JvmField protected val context: Context, protected val att get() = attachment.isInProgress val isPendingDownload: Boolean - get() = transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED || - transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING + get() = transferState == AttachmentState.FAILED.value || + transferState == AttachmentState.PENDING.value - val transferState: Int + val isDone: Boolean + get() = transferState == AttachmentState.DONE.value + + val isFailed: Boolean + get() = transferState == AttachmentState.FAILED.value + + val isExpired: Boolean + get() = transferState == AttachmentState.EXPIRED.value + + private val transferState: Int get() = attachment.transferState @DrawableRes @@ -160,7 +167,7 @@ abstract class Slide(@JvmField protected val context: Context, protected val att uri, if (hasThumbnail) uri else null, resolvedType!!, - AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED, + AttachmentState.DOWNLOADING.value, size, width, height, diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/SlideDeck.java b/app/src/main/java/org/thoughtcrime/securesms/mms/SlideDeck.java index a559417e316..7c91fdcce60 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/SlideDeck.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/SlideDeck.java @@ -125,6 +125,16 @@ public boolean containsMediaSlide() { return null; } + public boolean hasVideo() { + for (Slide slide : slides) { + if (slide.hasVideo()) { + return true; + } + } + + return false; + } + public @Nullable TextSlide getTextSlide() { for (Slide slide: slides) { if (MediaUtil.isLongTextType(slide.getContentType())) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/AppBar.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/AppBar.kt index 2f3b3a547ff..a9a1d55d963 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/AppBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/AppBar.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import network.loki.messenger.R @@ -29,6 +30,7 @@ import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider import org.thoughtcrime.securesms.ui.theme.ThemeColors +import kotlin.math.sin @OptIn(ExperimentalMaterial3Api::class) @Preview @@ -71,6 +73,7 @@ fun AppBarPreview( fun BasicAppBar( title: String, modifier: Modifier = Modifier, + singleLine: Boolean = false, scrollBehavior: TopAppBarScrollBehavior? = null, backgroundColor: Color = LocalColors.current.background, navigationIcon: @Composable () -> Unit = {}, @@ -79,7 +82,7 @@ fun BasicAppBar( CenterAlignedTopAppBar( modifier = modifier, title = { - AppBarText(title = title) + AppBarText(title = title, singleLine = singleLine) }, colors = appBarColors(backgroundColor), navigationIcon = navigationIcon, @@ -118,6 +121,7 @@ fun BackAppBar( fun ActionAppBar( title: String, modifier: Modifier = Modifier, + singleLine: Boolean = false, scrollBehavior: TopAppBarScrollBehavior? = null, backgroundColor: Color = LocalColors.current.background, actionMode: Boolean = false, @@ -130,7 +134,7 @@ fun ActionAppBar( modifier = modifier, title = { if (!actionMode) { - AppBarText(title = title) + AppBarText(title = title, singleLine = singleLine) } }, navigationIcon = { @@ -140,7 +144,7 @@ fun ActionAppBar( verticalAlignment = Alignment.CenterVertically ) { navigationIcon() - AppBarText(title = actionModeTitle) + AppBarText(title = actionModeTitle, singleLine = singleLine) } } else { navigationIcon() @@ -159,8 +163,13 @@ fun ActionAppBar( } @Composable -fun AppBarText(title: String) { - Text(text = title, style = LocalType.current.h4) +fun AppBarText(title: String, singleLine: Boolean = false) { + Text( + text = title, + style = LocalType.current.h4, + maxLines = if(singleLine) 1 else Int.MAX_VALUE, + overflow = if(singleLine) TextOverflow.Ellipsis else TextOverflow.Clip + ) } @Composable diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/AttachmentUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/AttachmentUtil.kt index 398bcbcc04f..3e85a3de709 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/AttachmentUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/AttachmentUtil.kt @@ -3,31 +3,13 @@ package org.thoughtcrime.securesms.util import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.jobs.AttachmentDownloadJob import org.session.libsession.messaging.jobs.JobQueue -import org.session.libsession.messaging.sending_receiving.attachments.Attachment -import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment -private const val ZERO_SIZE = "0.00" -private const val KILO_SIZE = 1024f -private const val MB_SUFFIX = "MB" -private const val KB_SUFFIX = "KB" - -fun Attachment.displaySize(): String { - - val kbSize = size / KILO_SIZE - val needsMb = kbSize > KILO_SIZE - val sizeText = "%.2f".format(if (needsMb) kbSize / KILO_SIZE else kbSize) - val displaySize = when { - sizeText == ZERO_SIZE -> "0.01" - sizeText.endsWith(".00") -> sizeText.takeWhile { it != '.' } - else -> sizeText - } - return "$displaySize${if (needsMb) MB_SUFFIX else KB_SUFFIX}" -} fun JobQueue.createAndStartAttachmentDownload(attachment: DatabaseAttachment) { val attachmentId = attachment.attachmentId.rowId - if (attachment.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING + if (attachment.transferState == AttachmentState.PENDING.value && MessagingModuleConfiguration.shared.storage.getAttachmentUploadJob(attachmentId) == null) { // start download add(AttachmentDownloadJob(attachmentId, attachment.mmsId)) diff --git a/app/src/main/res/drawable/call_message_background.xml b/app/src/main/res/drawable/call_message_background.xml deleted file mode 100644 index 77139091675..00000000000 --- a/app/src/main/res/drawable/call_message_background.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_download_circle_filled_48.xml b/app/src/main/res/drawable/ic_download_circle_filled_48.xml deleted file mode 100644 index 78808945a1a..00000000000 --- a/app/src/main/res/drawable/ic_download_circle_filled_48.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/message_bubble_background_received_alone.xml b/app/src/main/res/drawable/message_bubble_background_received.xml similarity index 79% rename from app/src/main/res/drawable/message_bubble_background_received_alone.xml rename to app/src/main/res/drawable/message_bubble_background_received.xml index 5e657c9793b..c509652f94c 100644 --- a/app/src/main/res/drawable/message_bubble_background_received_alone.xml +++ b/app/src/main/res/drawable/message_bubble_background_received.xml @@ -4,7 +4,7 @@ - + diff --git a/app/src/main/res/layout/dialog_clear_all_data.xml b/app/src/main/res/layout/dialog_clear_all_data.xml index 0d60ea8551d..b0f45ba8970 100644 --- a/app/src/main/res/layout/dialog_clear_all_data.xml +++ b/app/src/main/res/layout/dialog_clear_all_data.xml @@ -69,7 +69,6 @@ android:visibility="gone" tools:visibility="visible"/> - \ No newline at end of file diff --git a/app/src/main/res/layout/thumbnail_view.xml b/app/src/main/res/layout/thumbnail_view.xml index 7ea80eb11c3..ce6575f7a33 100644 --- a/app/src/main/res/layout/thumbnail_view.xml +++ b/app/src/main/res/layout/thumbnail_view.xml @@ -17,24 +17,6 @@ android:scaleType="center" android:contentDescription="@string/AccessibilityId_mediaMessage" /> - - - - + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_control_message.xml b/app/src/main/res/layout/view_control_message.xml index 1f68f22921b..3e006f1502d 100644 --- a/app/src/main/res/layout/view_control_message.xml +++ b/app/src/main/res/layout/view_control_message.xml @@ -71,7 +71,9 @@ android:gravity="center" android:textAlignment="center" android:textColor="?message_received_text_color" + android:textSize="@dimen/medium_font_size" app:drawableStartCompat="@drawable/ic_phone_missed" + android:drawablePadding="@dimen/very_small_spacing" tools:text="You missed a call" /> diff --git a/app/src/main/res/layout/view_conversation_typing_container.xml b/app/src/main/res/layout/view_conversation_typing_container.xml index ef5cda506db..d2097f95c6f 100644 --- a/app/src/main/res/layout/view_conversation_typing_container.xml +++ b/app/src/main/res/layout/view_conversation_typing_container.xml @@ -14,8 +14,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="7dp" - android:background="@drawable/message_bubble_background_received_alone" - android:backgroundTint="?message_received_background_color"> + android:background="@drawable/message_bubble_background_received"> + app:tint="?android:textColorTertiary" /> diff --git a/app/src/main/res/layout/view_document.xml b/app/src/main/res/layout/view_document.xml index e8e6b15ab67..c9abfd0f55a 100644 --- a/app/src/main/res/layout/view_document.xml +++ b/app/src/main/res/layout/view_document.xml @@ -6,34 +6,57 @@ android:layout_height="wrap_content" xmlns:tools="http://schemas.android.com/tools" android:orientation="horizontal" - android:paddingHorizontal="@dimen/medium_spacing" - android:paddingVertical="12dp" + android:paddingEnd="@dimen/message_spacing" android:gravity="center" android:contentDescription="@string/AccessibilityId_document"> - + + - + + - + android:orientation="vertical" + android:paddingVertical="@dimen/small_spacing" + android:layout_marginStart="@dimen/message_spacing"> + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_open_group_invitation.xml b/app/src/main/res/layout/view_open_group_invitation.xml index 6bc3acab2f1..032bbff148f 100644 --- a/app/src/main/res/layout/view_open_group_invitation.xml +++ b/app/src/main/res/layout/view_open_group_invitation.xml @@ -5,8 +5,8 @@ android:layout_height="wrap_content" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" - android:paddingHorizontal="@dimen/medium_spacing" - android:paddingVertical="12dp" + android:paddingHorizontal="@dimen/message_spacing" + android:paddingVertical="@dimen/message_spacing" android:orientation="horizontal" android:gravity="center_vertical"> @@ -30,7 +30,7 @@ diff --git a/app/src/main/res/layout/view_pending_attachment.xml b/app/src/main/res/layout/view_pending_attachment.xml deleted file mode 100644 index a9131776515..00000000000 --- a/app/src/main/res/layout/view_pending_attachment.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/view_quote.xml b/app/src/main/res/layout/view_quote.xml index e5244cc8893..768c22eae09 100644 --- a/app/src/main/res/layout/view_quote.xml +++ b/app/src/main/res/layout/view_quote.xml @@ -6,8 +6,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#F00" - android:paddingTop="12dp" - android:paddingHorizontal="12dp" + android:paddingHorizontal="@dimen/message_spacing" + android:paddingTop="@dimen/message_spacing" app:quote_mode="regular"> diff --git a/app/src/main/res/layout/view_visible_message_content.xml b/app/src/main/res/layout/view_visible_message_content.xml index 2a7f6e4d0a4..c748148c8c9 100644 --- a/app/src/main/res/layout/view_visible_message_content.xml +++ b/app/src/main/res/layout/view_visible_message_content.xml @@ -77,10 +77,10 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:maxWidth="@dimen/max_text_width" - android:paddingHorizontal="12dp" - android:paddingVertical="@dimen/small_spacing" android:visibility="gone" tools:visibility="visible" + android:paddingHorizontal="@dimen/message_spacing" + android:paddingVertical="@dimen/small_spacing" app:layout_constraintTop_toBottomOf="@+id/bodyTopBarrier" app:layout_constraintStart_toStartOf="parent" /> @@ -93,7 +93,6 @@ app:layout_constraintStart_toStartOf="parent"/> - - - + + 4dp 8dp + 12dp 16dp 24dp 35dp diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index c59b90b3b53..f46ad041b40 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -243,14 +243,13 @@ diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt index c07b527aa84..31c9b79980f 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt @@ -14,9 +14,9 @@ import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.DecodedAudio import org.session.libsession.utilities.DownloadUtilities import org.session.libsession.utilities.InputStreamMediaDataSource -import org.session.libsignal.exceptions.NonRetryableException import org.session.libsignal.streams.AttachmentCipherInputStream import org.session.libsignal.utilities.Base64 +import org.session.libsignal.utilities.HTTP import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.ByteArraySlice.Companion.write import java.io.File @@ -76,8 +76,15 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) val threadID = storage.getThreadIdForMms(databaseMessageID) val handleFailure: (java.lang.Exception, attachmentId: AttachmentId?) -> Unit = { exception, attachment -> - if (exception is NonRetryableException || - exception == Error.NoAttachment + if(exception is HTTP.HTTPRequestFailedException && exception.statusCode == 404){ + attachment?.let { id -> + Log.d("AttachmentDownloadJob", "Setting attachment state = failed, have attachment") + messageDataProvider.setAttachmentState(AttachmentState.EXPIRED, id, databaseMessageID) + } ?: run { + Log.d("AttachmentDownloadJob", "Setting attachment state = failed, don't have attachment") + messageDataProvider.setAttachmentState(AttachmentState.EXPIRED, AttachmentId(attachmentID,0), databaseMessageID) + } + } else if (exception == Error.NoAttachment || exception == Error.NoThread || exception == Error.NoSender || (exception is OnionRequestAPI.HTTPRequestFailedAtDestinationException && exception.statusCode == 400)) { @@ -123,14 +130,16 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) } var tempFile: File? = null + var attachment: DatabaseAttachment? = null + try { - val attachment = messageDataProvider.getDatabaseAttachment(attachmentID) + attachment = messageDataProvider.getDatabaseAttachment(attachmentID) ?: return handleFailure(Error.NoAttachment, null) if (attachment.hasData()) { handleFailure(Error.DuplicateData, attachment.attachmentId) return } - messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachment.attachmentId, this.databaseMessageID) + messageDataProvider.setAttachmentState(AttachmentState.DOWNLOADING, attachment.attachmentId, this.databaseMessageID) tempFile = createTempFile() val openGroup = storage.getOpenGroup(threadID) if (openGroup == null) { @@ -171,7 +180,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) } catch (e: Exception) { Log.e("AttachmentDownloadJob", "Error processing attachment download", e) tempFile?.delete() - return handleFailure(e,null) + return handleFailure(e,attachment?.attachmentId) } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/RetrieveProfileAvatarJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/RetrieveProfileAvatarJob.kt index 063d5c52428..963201c9784 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/RetrieveProfileAvatarJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/RetrieveProfileAvatarJob.kt @@ -12,6 +12,7 @@ import org.session.libsession.utilities.Util.equals import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.exceptions.NonRetryableException import org.session.libsignal.streams.ProfileCipherInputStream +import org.session.libsignal.utilities.HTTP import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Util.SECURE_RANDOM import java.io.File @@ -99,11 +100,17 @@ class RetrieveProfileAvatarJob( return delegate.handleJobFailedPermanently(this, dispatcherName, e) } catch (e: Exception) { - Log.e("Loki", "Failed to download profile avatar", e) - if (failureCount + 1 >= maxFailureCount) { + if(e is HTTP.HTTPRequestFailedException && e.statusCode == 404){ + Log.e("Loki", "Failed to download profile avatar from non-retryable error", e) errorUrls += profileAvatar + return delegate.handleJobFailedPermanently(this, dispatcherName, e) + } else { + Log.e("Loki", "Failed to download profile avatar", e) + if (failureCount + 1 >= maxFailureCount) { + errorUrls += profileAvatar + } + return delegate.handleJobFailed(this, dispatcherName, e) } - return delegate.handleJobFailed(this, dispatcherName, e) } finally { downloadDestination.delete() } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/Attachment.java b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/Attachment.java index 5d7e5025c2a..c9438bb7cfc 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/Attachment.java +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/Attachment.java @@ -54,9 +54,15 @@ public Attachment(@NonNull String contentType, int transferState, long size, Str public int getTransferState() { return transferState; } public boolean isInProgress() { - return transferState != AttachmentTransferProgress.TRANSFER_PROGRESS_DONE && - transferState != AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED && - transferState != AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING; + return transferState == AttachmentState.DOWNLOADING.getValue(); + } + + public boolean isDone() { + return transferState == AttachmentState.DONE.getValue(); + } + + public boolean isFailed() { + return transferState == AttachmentState.FAILED.getValue(); } public long getSize() { return size; } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/AttachmentTransferProgress.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/AttachmentTransferProgress.kt deleted file mode 100644 index 76b1e57bbbb..00000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/AttachmentTransferProgress.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.session.libsession.messaging.sending_receiving.attachments - -object AttachmentTransferProgress { - const val TRANSFER_PROGRESS_DONE = 0 - const val TRANSFER_PROGRESS_STARTED = 1 - const val TRANSFER_PROGRESS_PENDING = 2 - const val TRANSFER_PROGRESS_FAILED = 3 -} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/PointerAttachment.java b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/PointerAttachment.java index 69e08277f4e..662795ddf07 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/PointerAttachment.java +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/PointerAttachment.java @@ -100,24 +100,24 @@ public static Optional forPointer(Optional } return Optional.of(new PointerAttachment(pointer.get().getContentType(), - AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING, - pointer.get().asPointer().getSize().or(0), - pointer.get().asPointer().getFilename(), - String.valueOf(pointer.get().asPointer().getId()), - encodedKey, null, - pointer.get().asPointer().getDigest().orNull(), - fastPreflightId, - pointer.get().asPointer().getVoiceNote(), - pointer.get().asPointer().getWidth(), - pointer.get().asPointer().getHeight(), - pointer.get().asPointer().getCaption().orNull(), - pointer.get().asPointer().getUrl())); + AttachmentState.PENDING.getValue(), + pointer.get().asPointer().getSize().or(0), + pointer.get().asPointer().getFilename(), + String.valueOf(pointer.get().asPointer().getId()), + encodedKey, null, + pointer.get().asPointer().getDigest().orNull(), + fastPreflightId, + pointer.get().asPointer().getVoiceNote(), + pointer.get().asPointer().getWidth(), + pointer.get().asPointer().getHeight(), + pointer.get().asPointer().getCaption().orNull(), + pointer.get().asPointer().getUrl())); } public static Optional forPointer(SignalServiceProtos.AttachmentPointer pointer) { return Optional.of(new PointerAttachment(pointer.getContentType(), - AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING, + AttachmentState.PENDING.getValue(), (long)pointer.getSize(), pointer.getFileName(), String.valueOf(pointer != null ? pointer.getId() : 0), @@ -136,26 +136,26 @@ public static Optional forPointer(SignalServiceProtos.DataMessage.Qu SignalServiceProtos.AttachmentPointer thumbnail = pointer.getThumbnail(); return Optional.of(new PointerAttachment(pointer.getContentType(), - AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING, - thumbnail != null ? (long)thumbnail.getSize() : 0, - thumbnail.getFileName(), - String.valueOf(thumbnail != null ? thumbnail.getId() : 0), - thumbnail != null && thumbnail.getKey() != null ? Base64.encodeBytes(thumbnail.getKey().toByteArray()) : null, - null, - thumbnail != null ? thumbnail.getDigest().toByteArray() : null, - null, - false, - thumbnail != null ? thumbnail.getWidth() : 0, - thumbnail != null ? thumbnail.getHeight() : 0, - thumbnail != null ? thumbnail.getCaption() : null, - thumbnail != null ? thumbnail.getUrl() : "")); + AttachmentState.PENDING.getValue(), + thumbnail != null ? (long)thumbnail.getSize() : 0, + thumbnail.getFileName(), + String.valueOf(thumbnail != null ? thumbnail.getId() : 0), + thumbnail != null && thumbnail.getKey() != null ? Base64.encodeBytes(thumbnail.getKey().toByteArray()) : null, + null, + thumbnail != null ? thumbnail.getDigest().toByteArray() : null, + null, + false, + thumbnail != null ? thumbnail.getWidth() : 0, + thumbnail != null ? thumbnail.getHeight() : 0, + thumbnail != null ? thumbnail.getCaption() : null, + thumbnail != null ? thumbnail.getUrl() : "")); } public static Optional forPointer(SignalServiceDataMessage.Quote.QuotedAttachment pointer) { SignalServiceAttachment thumbnail = pointer.getThumbnail(); return Optional.of(new PointerAttachment(pointer.getContentType(), - AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING, + AttachmentState.PENDING.getValue(), thumbnail != null ? thumbnail.asPointer().getSize().or(0) : 0, pointer.getFileName(), String.valueOf(thumbnail != null ? thumbnail.asPointer().getId() : 0), @@ -178,7 +178,7 @@ public static Optional forPointer(SignalServiceDataMessage.Quote.Quo public static Attachment forAttachment(org.session.libsession.messaging.messages.visible.Attachment attachment) { return new PointerAttachment( attachment.getContentType(), - AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING, + AttachmentState.PENDING.getValue(), attachment.getSizeInBytes(), attachment.getFilename(), null, Base64.encodeBytes(attachment.getKey()), diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/SessionServiceAttachment.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/SessionServiceAttachment.kt index c30e628cde0..1f493a0a867 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/SessionServiceAttachment.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/SessionServiceAttachment.kt @@ -100,10 +100,10 @@ abstract class SessionServiceAttachment protected constructor(val contentType: S } } -// matches values in AttachmentDatabase.java enum class AttachmentState(val value: Int) { DONE(0), - STARTED(1), + DOWNLOADING(1), PENDING(2), - FAILED(3) + FAILED(3), + EXPIRED(4) } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/utilities/Contact.java b/libsession/src/main/java/org/session/libsession/utilities/Contact.java index a0d181ad623..4bd0c1e01a8 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/Contact.java +++ b/libsession/src/main/java/org/session/libsession/utilities/Contact.java @@ -13,7 +13,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; -import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState; import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId; import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment; import org.session.libsignal.utilities.JsonUtil; @@ -642,7 +642,7 @@ public int describeContents() { private static Attachment attachmentFromUri(@Nullable Uri uri) { if (uri == null) return null; - return new UriAttachment(uri, MediaTypes.IMAGE_JPEG, AttachmentTransferProgress.TRANSFER_PROGRESS_DONE, 0, null, false, false, null); + return new UriAttachment(uri, MediaTypes.IMAGE_JPEG, AttachmentState.DONE.getValue(), 0, null, false, false, null); } @Override diff --git a/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt b/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt index cf581d0f01d..585abe3353d 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt @@ -29,9 +29,6 @@ object DownloadUtilities { downloadFile(outputStream, url) return // return on success } catch (e: HTTP.HTTPRequestFailedException) { - if (e.statusCode == 404) { - throw NonRetryableException("404 response trying to download file: $url", e) - } exception = e } catch (e: Exception) { exception = e From 1106987c0c859e9761d2973df185a0a1f1d61d59 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 28 Mar 2025 10:33:01 +1030 Subject: [PATCH 04/43] Android target sdk 35 (#1063) * Android target sdk 15 Removed unused libraries Fixed broken icon colors * Bumping libsession-util --- app/build.gradle | 22 +++++-------------- app/src/main/AndroidManifest.xml | 2 +- .../securesms/ApplicationContext.kt | 11 +++++----- .../menus/ConversationActionModeCallback.kt | 18 ++++++++++++++- .../conversation/v2/messages/DocumentView.kt | 1 + app/src/main/res/layout/share_activity.xml | 10 ++++----- app/src/main/res/layout/view_document.xml | 2 +- .../menu/menu_conversation_item_action.xml | 1 + build.gradle | 10 ++------- gradle.properties | 6 ++--- libsession/build.gradle | 2 +- 11 files changed, 43 insertions(+), 42 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f4d9c11a7a5..2c407f426e0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -270,7 +270,7 @@ dependencies { implementation("com.google.dagger:hilt-android:$daggerHiltVersion") implementation "androidx.appcompat:appcompat:$appcompatVersion" - implementation 'androidx.recyclerview:recyclerview:1.3.2' + implementation 'androidx.recyclerview:recyclerview:1.4.0' implementation "com.google.android.material:material:$materialVersion" implementation 'com.google.android.flexbox:flexbox:3.0.0' implementation 'androidx.legacy:legacy-support-v13:1.0.0' @@ -279,7 +279,7 @@ dependencies { implementation 'androidx.legacy:legacy-preference-v14:1.0.0' implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.exifinterface:exifinterface:1.3.4' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.constraintlayout:constraintlayout:2.2.1' implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" @@ -287,11 +287,11 @@ dependencies { implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" implementation "androidx.paging:paging-runtime-ktx:$pagingVersion" - implementation 'androidx.activity:activity-ktx:1.9.2' - implementation 'androidx.activity:activity-compose:1.9.2' - implementation 'androidx.fragment:fragment-ktx:1.8.4' + implementation 'androidx.activity:activity-ktx:1.10.1' + implementation 'androidx.activity:activity-compose:1.10.1' + implementation 'androidx.fragment:fragment-ktx:1.8.6' implementation "androidx.core:core-ktx:$coreVersion" - implementation "androidx.work:work-runtime-ktx:2.7.1" + implementation "androidx.work:work-runtime-ktx:2.10.0" playImplementation ("com.google.firebase:firebase-messaging:24.0.0") { exclude group: 'com.google.firebase', module: 'firebase-core' @@ -307,19 +307,13 @@ dependencies { implementation 'org.signal:aesgcmprovider:0.0.3' implementation 'io.github.webrtc-sdk:android:125.6422.06.1' implementation "me.leolin:ShortcutBadger:1.1.16" - implementation 'se.emilsjolander:stickylistheaders:2.7.0' - implementation 'com.jpardogo.materialtabstrip:library:1.0.9' implementation 'org.apache.httpcomponents:httpclient-android:4.3.5' - implementation 'commons-net:commons-net:3.7.2' implementation 'com.github.chrisbanes:PhotoView:2.1.3' implementation "com.github.bumptech.glide:glide:$glideVersion" implementation "com.github.bumptech.glide:compose:1.0.0-beta01" implementation 'com.makeramen:roundedimageview:2.1.0' - implementation 'com.pnikosis:materialish-progress:1.5' implementation 'org.greenrobot:eventbus:3.0.0' - implementation 'pl.tajchert:waitingdots:0.1.0' implementation 'com.vanniktech:android-image-cropper:4.5.0' - implementation 'com.melnykov:floatingactionbutton:1.3.0' implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') { exclude group: 'com.android.support', module: 'support-annotations' } @@ -331,7 +325,6 @@ dependencies { exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection' } implementation 'com.annimon:stream:1.1.8' - implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2' implementation 'androidx.sqlite:sqlite-ktx:2.3.1' implementation 'net.zetetic:sqlcipher-android:4.6.1@aar' implementation project(":libsignal") @@ -363,9 +356,6 @@ dependencies { // Core library androidTestImplementation "androidx.test:core:$testCoreVersion" - androidTestImplementation('com.adevinta.android:barista:4.2.0') { - exclude group: 'org.jetbrains.kotlin' - } // AndroidJUnitRunner and JUnit Rules androidTestImplementation 'androidx.test:runner:1.5.2' androidTestImplementation 'androidx.test:rules:1.5.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d6c9072e18d..12a36085def 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,7 +3,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> - + - + diff --git a/app/src/main/res/layout/view_document.xml b/app/src/main/res/layout/view_document.xml index c9abfd0f55a..95b99217404 100644 --- a/app/src/main/res/layout/view_document.xml +++ b/app/src/main/res/layout/view_document.xml @@ -15,7 +15,7 @@ android:layout_height="match_parent" android:paddingHorizontal="@dimen/message_spacing" android:background="@drawable/view_quote_attachment_preview_background"> - Date: Fri, 28 Mar 2025 15:30:39 +1100 Subject: [PATCH 05/43] [SES-3368] - Convert MediaSendFragment to Kotlin (#1064) --- .../mediasend/MediaSendFragment.java | 464 ---------------- .../securesms/mediasend/MediaSendFragment.kt | 498 ++++++++++++++++++ 2 files changed, 498 insertions(+), 464 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java deleted file mode 100644 index 235a4e9dfa1..00000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java +++ /dev/null @@ -1,464 +0,0 @@ -package org.thoughtcrime.securesms.mediasend; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Rect; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.view.WindowManager; -import android.view.inputmethod.EditorInfo; -import android.widget.ImageButton; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.view.ContextThemeWrapper; -import androidx.fragment.app.Fragment; -import androidx.lifecycle.ViewModelProvider; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.viewpager.widget.ViewPager; -import com.bumptech.glide.Glide; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.ExecutionException; - -import dagger.hilt.android.AndroidEntryPoint; -import network.loki.messenger.R; -import org.session.libsession.utilities.MediaTypes; -import org.session.libsession.utilities.TextSecurePreferences; -import org.session.libsession.utilities.Util; -import org.session.libsession.utilities.recipients.Recipient; -import org.session.libsignal.utilities.ListenableFuture; -import org.session.libsignal.utilities.Log; -import org.session.libsignal.utilities.SettableFuture; -import org.session.libsignal.utilities.guava.Optional; -import org.thoughtcrime.securesms.components.ComposeText; -import org.thoughtcrime.securesms.components.ControllableViewPager; -import org.thoughtcrime.securesms.components.InputAwareLayout; -import org.thoughtcrime.securesms.imageeditor.model.EditorModel; -import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter; -import org.thoughtcrime.securesms.providers.BlobProvider; -import org.thoughtcrime.securesms.scribbles.ImageEditorFragment; -import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState; -import org.thoughtcrime.securesms.util.PushCharacterCalculator; -import org.thoughtcrime.securesms.util.Stopwatch; - -/** - * Allows the user to edit and caption a set of media items before choosing to send them. - */ -@AndroidEntryPoint -public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGlobalLayoutListener, - MediaRailAdapter.RailItemListener, - InputAwareLayout.OnKeyboardShownListener, - InputAwareLayout.OnKeyboardHiddenListener -{ - private static final String TAG = MediaSendFragment.class.getSimpleName(); - - private static final String KEY_ADDRESS = "address"; - - private InputAwareLayout hud; - private View captionAndRail; - private ImageButton sendButton; - private ComposeText composeText; - private ViewGroup composeContainer; - private ViewGroup playbackControlsContainer; - private TextView charactersLeft; - private View closeButton; - private View loader; - - private ControllableViewPager fragmentPager; - private MediaSendFragmentPagerAdapter fragmentPagerAdapter; - private RecyclerView mediaRail; - private MediaRailAdapter mediaRailAdapter; - - private int visibleHeight; - private MediaSendViewModel viewModel; - private Controller controller; - - private final Rect visibleBounds = new Rect(); - - private final PushCharacterCalculator characterCalculator = new PushCharacterCalculator(); - - public static MediaSendFragment newInstance(@NonNull Recipient recipient) { - Bundle args = new Bundle(); - args.putParcelable(KEY_ADDRESS, recipient.getAddress()); - - MediaSendFragment fragment = new MediaSendFragment(); - fragment.setArguments(args); - return fragment; - } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - - if (!(requireActivity() instanceof Controller)) { - throw new IllegalStateException("Parent activity must implement controller interface."); - } - - controller = (Controller) requireActivity(); - viewModel = new ViewModelProvider(requireActivity()).get(MediaSendViewModel.class); - } - - @Override - public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.mediasend_fragment, container, false); - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - initViewModel(); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - hud = view.findViewById(R.id.mediasend_hud); - captionAndRail = view.findViewById(R.id.mediasend_caption_and_rail); - sendButton = view.findViewById(R.id.mediasend_send_button); - composeText = view.findViewById(R.id.mediasend_compose_text); - composeContainer = view.findViewById(R.id.mediasend_compose_container); - fragmentPager = view.findViewById(R.id.mediasend_pager); - mediaRail = view.findViewById(R.id.mediasend_media_rail); - playbackControlsContainer = view.findViewById(R.id.mediasend_playback_controls_container); - charactersLeft = view.findViewById(R.id.mediasend_characters_left); - closeButton = view.findViewById(R.id.mediasend_close_button); - loader = view.findViewById(R.id.loader); - - View sendButtonBkg = view.findViewById(R.id.mediasend_send_button_bkg); - - sendButton.setOnClickListener(v -> { - if (hud.isKeyboardOpen()) { - hud.hideSoftkey(composeText, null); - } - - processMedia(fragmentPagerAdapter.getAllMedia(), fragmentPagerAdapter.getSavedState()); - }); - - ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener(); - - composeText.setOnKeyListener(composeKeyPressedListener); - composeText.addTextChangedListener(composeKeyPressedListener); - composeText.setOnClickListener(composeKeyPressedListener); - composeText.setOnFocusChangeListener(composeKeyPressedListener); - - composeText.requestFocus(); - - fragmentPagerAdapter = new MediaSendFragmentPagerAdapter(getChildFragmentManager()); - fragmentPager.setAdapter(fragmentPagerAdapter); - - FragmentPageChangeListener pageChangeListener = new FragmentPageChangeListener(); - fragmentPager.addOnPageChangeListener(pageChangeListener); - fragmentPager.post(() -> pageChangeListener.onPageSelected(fragmentPager.getCurrentItem())); - - mediaRailAdapter = new MediaRailAdapter(Glide.with(this), this, true); - mediaRail.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); - mediaRail.setAdapter(mediaRailAdapter); - - hud.getRootView().getViewTreeObserver().addOnGlobalLayoutListener(this); - hud.addOnKeyboardShownListener(this); - hud.addOnKeyboardHiddenListener(this); - - composeText.append(viewModel.getBody()); - - Recipient recipient = Recipient.from(requireContext(), getArguments().getParcelable(KEY_ADDRESS), false); - String displayName = Optional.fromNullable(recipient.getName()) - .or(Optional.fromNullable(recipient.getProfileName()) - .or(recipient.getAddress().toString())); - composeText.setHint(getString(R.string.message), null); - composeText.setOnEditorActionListener((v, actionId, event) -> { - boolean isSend = actionId == EditorInfo.IME_ACTION_SEND; - if (isSend) sendButton.performClick(); - return isSend; - }); - - closeButton.setOnClickListener(v -> requireActivity().onBackPressed()); - } - - @Override - public void onStart() { - super.onStart(); - - fragmentPagerAdapter.restoreState(viewModel.getDrawState()); - viewModel.onImageEditorStarted(); - - requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); - } - - @Override - public void onHiddenChanged(boolean hidden) { - super.onHiddenChanged(hidden); - } - - @Override - public void onStop() { - super.onStop(); - fragmentPagerAdapter.saveAllState(); - viewModel.saveDrawState(fragmentPagerAdapter.getSavedState()); - } - - @Override - public void onGlobalLayout() { - hud.getRootView().getWindowVisibleDisplayFrame(visibleBounds); - - int currentVisibleHeight = visibleBounds.height(); - - if (currentVisibleHeight != visibleHeight) { - hud.getLayoutParams().height = currentVisibleHeight; - hud.layout(visibleBounds.left, visibleBounds.top, visibleBounds.right, visibleBounds.bottom); - hud.requestLayout(); - - visibleHeight = currentVisibleHeight; - } - } - - @Override - public void onRailItemClicked(int distanceFromActive) { - viewModel.onPageChanged(fragmentPager.getCurrentItem() + distanceFromActive); - } - - @Override - public void onRailItemDeleteClicked(int distanceFromActive) { - viewModel.onMediaItemRemoved(requireContext(), fragmentPager.getCurrentItem() + distanceFromActive); - } - - @Override - public void onKeyboardShown() { - if (composeText.hasFocus()) { - mediaRail.setVisibility(View.VISIBLE); - composeContainer.setVisibility(View.VISIBLE); - } else { - mediaRail.setVisibility(View.GONE); - composeContainer.setVisibility(View.VISIBLE); - } - } - - @Override - public void onKeyboardHidden() { - composeContainer.setVisibility(View.VISIBLE); - mediaRail.setVisibility(View.VISIBLE); - } - - public void onTouchEventsNeeded(boolean needed) { - if (fragmentPager != null) { - fragmentPager.setEnabled(!needed); - } - } - - public boolean handleBackPress() { - if (hud.isInputOpen()) { - hud.hideCurrentInput(composeText); - return true; - } - return false; - } - - private void initViewModel() { - viewModel.getSelectedMedia().observe(this, media -> { - if (Util.isEmpty(media)) { - controller.onNoMediaAvailable(); - return; - } - - fragmentPagerAdapter.setMedia(media); - - mediaRail.setVisibility(View.VISIBLE); - mediaRailAdapter.setMedia(media); - }); - - viewModel.getPosition().observe(this, position -> { - if (position == null || position < 0) return; - - fragmentPager.setCurrentItem(position, true); - mediaRailAdapter.setActivePosition(position); - mediaRail.smoothScrollToPosition(position); - - View playbackControls = fragmentPagerAdapter.getPlaybackControls(position); - - if (playbackControls != null) { - ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - playbackControls.setLayoutParams(params); - playbackControlsContainer.removeAllViews(); - playbackControlsContainer.addView(playbackControls); - } else { - playbackControlsContainer.removeAllViews(); - } - }); - - viewModel.getBucketId().observe(this, bucketId -> { - if (bucketId == null) return; - - mediaRailAdapter.setAddButtonListener(() -> controller.onAddMediaClicked(bucketId)); - }); - } - - - private void presentCharactersRemaining() { - String messageBody = composeText.getTextTrimmed(); - CharacterState characterState = characterCalculator.calculateCharacters(messageBody); - - if (characterState.charactersRemaining <= 15 || characterState.messagesSpent > 1) { - charactersLeft.setText(String.format(Locale.getDefault(), - "%d/%d (%d)", - characterState.charactersRemaining, - characterState.maxTotalMessageSize, - characterState.messagesSpent)); - charactersLeft.setVisibility(View.VISIBLE); - } else { - charactersLeft.setVisibility(View.GONE); - } - } - - @SuppressLint("StaticFieldLeak") - private void processMedia(@NonNull List mediaList, @NonNull Map savedState) { - Map> futures = new HashMap<>(); - - for (Media media : mediaList) { - Object state = savedState.get(media.getUri()); - - if (state instanceof ImageEditorFragment.Data) { - EditorModel model = ((ImageEditorFragment.Data) state).readModel(); - if (model != null && model.isChanged()) { - futures.put(media, render(requireContext(), model)); - } - } - } - - new AsyncTask>() { - - private Stopwatch renderTimer; - private Runnable progressTimer; - - @Override - protected void onPreExecute() { - renderTimer = new Stopwatch("ProcessMedia"); - progressTimer = () -> { - loader.setVisibility(View.VISIBLE); - }; - Util.runOnMainDelayed(progressTimer, 250); - } - - @Override - protected List doInBackground(Void... voids) { - Context context = requireContext(); - List updatedMedia = new ArrayList<>(mediaList.size()); - - for (Media media : mediaList) { - if (futures.containsKey(media)) { - try { - Bitmap bitmap = futures.get(media).get(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos); - - Uri uri = BlobProvider.getInstance() - .forData(baos.toByteArray()) - .withMimeType(MediaTypes.IMAGE_JPEG) - .createForSingleSessionOnDisk(context, e -> Log.w(TAG, "Failed to write to disk.", e)); - - Media updated = new Media(uri, media.getFilename(), MediaTypes.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), baos.size(), media.getBucketId(), media.getCaption()); - - updatedMedia.add(updated); - renderTimer.split("item"); - } catch (InterruptedException | ExecutionException | IOException e) { - Log.w(TAG, "Failed to render image. Using base image."); - updatedMedia.add(media); - } - } else { - updatedMedia.add(media); - } - } - return updatedMedia; - } - - @Override - protected void onPostExecute(List media) { - controller.onSendClicked(media, composeText.getTextTrimmed()); - Util.cancelRunnableOnMain(progressTimer); - loader.setVisibility(View.GONE); - renderTimer.stop(TAG); - } - }.execute(); - } - - private static ListenableFuture render(@NonNull Context context, @NonNull EditorModel model) { - SettableFuture future = new SettableFuture<>(); - - AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> future.set(model.render(context))); - - return future; - } - - public void onRequestFullScreen(boolean fullScreen) { - captionAndRail.setVisibility(fullScreen ? View.GONE : View.VISIBLE); - } - - private class FragmentPageChangeListener extends ViewPager.SimpleOnPageChangeListener { - @Override - public void onPageSelected(int position) { - viewModel.onPageChanged(position); - } - } - - private class ComposeKeyPressedListener implements View.OnKeyListener, View.OnClickListener, TextWatcher, View.OnFocusChangeListener { - - int beforeLength; - - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - if (event.getAction() == KeyEvent.ACTION_DOWN) { - if (keyCode == KeyEvent.KEYCODE_ENTER) { - if (TextSecurePreferences.isEnterSendsEnabled(requireContext())) { - sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER)); - sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER)); - return true; - } - } - } - return false; - } - - @Override - public void onClick(View v) { - hud.showSoftkey(composeText); - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count,int after) { - beforeLength = composeText.getTextTrimmed().length(); - } - - @Override - public void afterTextChanged(Editable s) { - presentCharactersRemaining(); - viewModel.onBodyChanged(s); - } - - @Override - public void onTextChanged(CharSequence s, int start, int before,int count) {} - - @Override - public void onFocusChange(View v, boolean hasFocus) {} - } - - public interface Controller { - void onAddMediaClicked(@NonNull String bucketId); - void onSendClicked(@NonNull List media, @NonNull String body); - void onNoMediaAvailable(); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.kt new file mode 100644 index 00000000000..58dca20cd20 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.kt @@ -0,0 +1,498 @@ +package org.thoughtcrime.securesms.mediasend + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Rect +import android.net.Uri +import android.os.AsyncTask +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.View.OnFocusChangeListener +import android.view.ViewGroup +import android.view.ViewTreeObserver.OnGlobalLayoutListener +import android.view.WindowManager +import android.view.inputmethod.EditorInfo +import android.widget.ImageButton +import android.widget.TextView +import android.widget.TextView.OnEditorActionListener +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.viewpager.widget.ViewPager.SimpleOnPageChangeListener +import com.bumptech.glide.Glide +import dagger.hilt.android.AndroidEntryPoint +import network.loki.messenger.R +import org.session.libsession.utilities.MediaTypes +import org.session.libsession.utilities.TextSecurePreferences.Companion.isEnterSendsEnabled +import org.session.libsession.utilities.Util.cancelRunnableOnMain +import org.session.libsession.utilities.Util.isEmpty +import org.session.libsession.utilities.Util.runOnMainDelayed +import org.session.libsession.utilities.recipients.Recipient +import org.session.libsignal.utilities.ListenableFuture +import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.SettableFuture +import org.session.libsignal.utilities.guava.Optional +import org.thoughtcrime.securesms.components.ComposeText +import org.thoughtcrime.securesms.components.ControllableViewPager +import org.thoughtcrime.securesms.components.InputAwareLayout +import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardHiddenListener +import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener +import org.thoughtcrime.securesms.imageeditor.model.EditorModel +import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter +import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter.RailItemListener +import org.thoughtcrime.securesms.mediasend.MediaSendViewModel +import org.thoughtcrime.securesms.providers.BlobProvider +import org.thoughtcrime.securesms.scribbles.ImageEditorFragment +import org.thoughtcrime.securesms.util.PushCharacterCalculator +import org.thoughtcrime.securesms.util.Stopwatch +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.util.Locale +import java.util.concurrent.ExecutionException + +/** + * Allows the user to edit and caption a set of media items before choosing to send them. + */ +@AndroidEntryPoint +class MediaSendFragment : Fragment(), OnGlobalLayoutListener, RailItemListener, + OnKeyboardShownListener, OnKeyboardHiddenListener { + private var hud: InputAwareLayout? = null + private var captionAndRail: View? = null + private var sendButton: ImageButton? = null + private var composeText: ComposeText? = null + private var composeContainer: ViewGroup? = null + private var playbackControlsContainer: ViewGroup? = null + private var charactersLeft: TextView? = null + private var closeButton: View? = null + private var loader: View? = null + + private var fragmentPager: ControllableViewPager? = null + private var fragmentPagerAdapter: MediaSendFragmentPagerAdapter? = null + private var mediaRail: RecyclerView? = null + private var mediaRailAdapter: MediaRailAdapter? = null + + private var visibleHeight = 0 + private var viewModel: MediaSendViewModel? = null + private var controller: Controller? = null + + private val visibleBounds = Rect() + + private val characterCalculator = PushCharacterCalculator() + + override fun onAttach(context: Context) { + super.onAttach(context) + + check(requireActivity() is Controller) { "Parent activity must implement controller interface." } + + controller = requireActivity() as Controller + viewModel = ViewModelProvider(requireActivity()).get( + MediaSendViewModel::class.java + ) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.mediasend_fragment, container, false) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + initViewModel() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + hud = view.findViewById(R.id.mediasend_hud) + captionAndRail = view.findViewById(R.id.mediasend_caption_and_rail) + sendButton = view.findViewById(R.id.mediasend_send_button) + composeText = view.findViewById(R.id.mediasend_compose_text) + composeContainer = view.findViewById(R.id.mediasend_compose_container) + fragmentPager = view.findViewById(R.id.mediasend_pager) + mediaRail = view.findViewById(R.id.mediasend_media_rail) + playbackControlsContainer = view.findViewById(R.id.mediasend_playback_controls_container) + charactersLeft = view.findViewById(R.id.mediasend_characters_left) + closeButton = view.findViewById(R.id.mediasend_close_button) + loader = view.findViewById(R.id.loader) + + val sendButtonBkg = view.findViewById(R.id.mediasend_send_button_bkg) + + sendButton!!.setOnClickListener(View.OnClickListener { v: View? -> + if (hud!!.isKeyboardOpen()) { + hud!!.hideSoftkey(composeText, null) + } + processMedia(fragmentPagerAdapter!!.allMedia, fragmentPagerAdapter!!.savedState) + }) + + val composeKeyPressedListener = ComposeKeyPressedListener() + + composeText!!.setOnKeyListener(composeKeyPressedListener) + composeText!!.addTextChangedListener(composeKeyPressedListener) + composeText!!.setOnClickListener(composeKeyPressedListener) + composeText!!.setOnFocusChangeListener(composeKeyPressedListener) + + composeText!!.requestFocus() + + fragmentPagerAdapter = MediaSendFragmentPagerAdapter(childFragmentManager) + fragmentPager!!.setAdapter(fragmentPagerAdapter) + + val pageChangeListener = FragmentPageChangeListener() + fragmentPager!!.addOnPageChangeListener(pageChangeListener) + fragmentPager!!.post(Runnable { pageChangeListener.onPageSelected(fragmentPager!!.currentItem) }) + + mediaRailAdapter = MediaRailAdapter(Glide.with(this), this, true) + mediaRail!!.setLayoutManager( + LinearLayoutManager( + requireContext(), + LinearLayoutManager.HORIZONTAL, + false + ) + ) + mediaRail!!.setAdapter(mediaRailAdapter) + + hud!!.getRootView().viewTreeObserver.addOnGlobalLayoutListener(this) + hud!!.addOnKeyboardShownListener(this) + hud!!.addOnKeyboardHiddenListener(this) + + composeText!!.append(viewModel!!.body) + + val recipient = Recipient.from( + requireContext(), + arguments!!.getParcelable(KEY_ADDRESS)!!, false + ) + val displayName = Optional.fromNullable(recipient.name) + .or( + Optional.fromNullable(recipient.profileName) + .or(recipient.address.toString()) + ) + composeText!!.setHint(getString(R.string.message), null) + composeText!!.setOnEditorActionListener(OnEditorActionListener { v: TextView?, actionId: Int, event: KeyEvent? -> + val isSend = actionId == EditorInfo.IME_ACTION_SEND + if (isSend) sendButton!!.performClick() + isSend + }) + + closeButton!!.setOnClickListener(View.OnClickListener { v: View? -> requireActivity().onBackPressed() }) + } + + override fun onStart() { + super.onStart() + + fragmentPagerAdapter!!.restoreState(viewModel!!.drawState) + viewModel!!.onImageEditorStarted() + + requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) + requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN) + } + + override fun onHiddenChanged(hidden: Boolean) { + super.onHiddenChanged(hidden) + } + + override fun onStop() { + super.onStop() + fragmentPagerAdapter!!.saveAllState() + viewModel!!.saveDrawState(fragmentPagerAdapter!!.savedState) + } + + override fun onGlobalLayout() { + hud!!.rootView.getWindowVisibleDisplayFrame(visibleBounds) + + val currentVisibleHeight = visibleBounds.height() + + if (currentVisibleHeight != visibleHeight) { + hud!!.layoutParams.height = currentVisibleHeight + hud!!.layout( + visibleBounds.left, + visibleBounds.top, + visibleBounds.right, + visibleBounds.bottom + ) + hud!!.requestLayout() + + visibleHeight = currentVisibleHeight + } + } + + override fun onRailItemClicked(distanceFromActive: Int) { + viewModel!!.onPageChanged(fragmentPager!!.currentItem + distanceFromActive) + } + + override fun onRailItemDeleteClicked(distanceFromActive: Int) { + viewModel!!.onMediaItemRemoved( + requireContext(), + fragmentPager!!.currentItem + distanceFromActive + ) + } + + override fun onKeyboardShown() { + if (composeText!!.hasFocus()) { + mediaRail!!.visibility = View.VISIBLE + composeContainer!!.visibility = View.VISIBLE + } else { + mediaRail!!.visibility = View.GONE + composeContainer!!.visibility = View.VISIBLE + } + } + + override fun onKeyboardHidden() { + composeContainer!!.visibility = View.VISIBLE + mediaRail!!.visibility = View.VISIBLE + } + + fun onTouchEventsNeeded(needed: Boolean) { + if (fragmentPager != null) { + fragmentPager!!.isEnabled = !needed + } + } + + fun handleBackPress(): Boolean { + if (hud!!.isInputOpen) { + hud!!.hideCurrentInput(composeText) + return true + } + return false + } + + private fun initViewModel() { + viewModel!!.getSelectedMedia().observe( + this + ) { media: List? -> + if (isEmpty(media)) { + controller!!.onNoMediaAvailable() + return@observe + } + fragmentPagerAdapter!!.setMedia(media!!) + + mediaRail!!.visibility = View.VISIBLE + mediaRailAdapter!!.setMedia(media) + } + + viewModel!!.getPosition().observe(this) { position: Int? -> + if (position == null || position < 0) return@observe + fragmentPager!!.setCurrentItem(position, true) + mediaRailAdapter!!.setActivePosition(position) + mediaRail!!.smoothScrollToPosition(position) + + val playbackControls = fragmentPagerAdapter!!.getPlaybackControls(position) + if (playbackControls != null) { + val params = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + playbackControls.layoutParams = params + playbackControlsContainer!!.removeAllViews() + playbackControlsContainer!!.addView(playbackControls) + } else { + playbackControlsContainer!!.removeAllViews() + } + } + + viewModel!!.getBucketId().observe(this) { bucketId: String? -> + if (bucketId == null) return@observe + mediaRailAdapter!!.setAddButtonListener { controller!!.onAddMediaClicked(bucketId) } + } + } + + + private fun presentCharactersRemaining() { + val messageBody = composeText!!.textTrimmed + val characterState = characterCalculator.calculateCharacters(messageBody) + + if (characterState.charactersRemaining <= 15 || characterState.messagesSpent > 1) { + charactersLeft!!.text = String.format( + Locale.getDefault(), + "%d/%d (%d)", + characterState.charactersRemaining, + characterState.maxTotalMessageSize, + characterState.messagesSpent + ) + charactersLeft!!.visibility = View.VISIBLE + } else { + charactersLeft!!.visibility = View.GONE + } + } + + @SuppressLint("StaticFieldLeak") + private fun processMedia(mediaList: List, savedState: Map) { + val futures: MutableMap> = HashMap() + + for (media in mediaList) { + val state = savedState[media.uri] + + if (state is ImageEditorFragment.Data) { + val model = state.readModel() + if (model != null && model.isChanged) { + futures[media] = render(requireContext(), model) + } + } + } + + object : AsyncTask>() { + private var renderTimer: Stopwatch? = null + private var progressTimer: Runnable? = null + + override fun onPreExecute() { + renderTimer = Stopwatch("ProcessMedia") + progressTimer = Runnable { + loader!!.visibility = View.VISIBLE + } + runOnMainDelayed(progressTimer!!, 250) + } + + override fun doInBackground(vararg params: Void?): List { + val context = requireContext() + val updatedMedia: MutableList = ArrayList(mediaList.size) + + for (media in mediaList) { + if (futures.containsKey(media)) { + try { + val bitmap = futures[media]!!.get() + val baos = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos) + + val uri = BlobProvider.getInstance() + .forData(baos.toByteArray()) + .withMimeType(MediaTypes.IMAGE_JPEG) + .createForSingleSessionOnDisk( + context + ) { e: IOException? -> + Log.w( + TAG, + "Failed to write to disk.", + e + ) + } + + val updated = Media( + uri, + media.filename, + MediaTypes.IMAGE_JPEG, + media.date, + bitmap.width, + bitmap.height, + baos.size().toLong(), + media.bucketId, + media.caption + ) + + updatedMedia.add(updated) + renderTimer!!.split("item") + } catch (e: InterruptedException) { + Log.w(TAG, "Failed to render image. Using base image.") + updatedMedia.add(media) + } catch (e: ExecutionException) { + Log.w(TAG, "Failed to render image. Using base image.") + updatedMedia.add(media) + } catch (e: IOException) { + Log.w(TAG, "Failed to render image. Using base image.") + updatedMedia.add(media) + } + } else { + updatedMedia.add(media) + } + } + return updatedMedia + } + + override fun onPostExecute(media: List) { + controller!!.onSendClicked(media, composeText!!.textTrimmed) + cancelRunnableOnMain(progressTimer!!) + loader!!.visibility = View.GONE + renderTimer!!.stop(TAG) + } + }.execute() + } + + fun onRequestFullScreen(fullScreen: Boolean) { + captionAndRail!!.visibility = + if (fullScreen) View.GONE else View.VISIBLE + } + + private inner class FragmentPageChangeListener : SimpleOnPageChangeListener() { + override fun onPageSelected(position: Int) { + viewModel!!.onPageChanged(position) + } + } + + private inner class ComposeKeyPressedListener : View.OnKeyListener, View.OnClickListener, + TextWatcher, OnFocusChangeListener { + var beforeLength: Int = 0 + + override fun onKey(v: View, keyCode: Int, event: KeyEvent): Boolean { + if (event.action == KeyEvent.ACTION_DOWN) { + if (keyCode == KeyEvent.KEYCODE_ENTER) { + if (isEnterSendsEnabled(requireContext())) { + sendButton!!.dispatchKeyEvent( + KeyEvent( + KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_ENTER + ) + ) + sendButton!!.dispatchKeyEvent( + KeyEvent( + KeyEvent.ACTION_UP, + KeyEvent.KEYCODE_ENTER + ) + ) + return true + } + } + } + return false + } + + override fun onClick(v: View) { + hud!!.showSoftkey(composeText) + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + beforeLength = composeText!!.textTrimmed.length + } + + override fun afterTextChanged(s: Editable) { + presentCharactersRemaining() + viewModel!!.onBodyChanged(s) + } + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + + override fun onFocusChange(v: View, hasFocus: Boolean) {} + } + + interface Controller { + fun onAddMediaClicked(bucketId: String) + fun onSendClicked(media: List, body: String) + fun onNoMediaAvailable() + } + + companion object { + private val TAG: String = MediaSendFragment::class.java.simpleName + + private const val KEY_ADDRESS = "address" + + fun newInstance(recipient: Recipient): MediaSendFragment { + val args = Bundle() + args.putParcelable(KEY_ADDRESS, recipient.address) + + val fragment = MediaSendFragment() + fragment.arguments = args + return fragment + } + + private fun render(context: Context, model: EditorModel): ListenableFuture { + val future = SettableFuture() + + AsyncTask.THREAD_POOL_EXECUTOR.execute { future.set(model.render(context)) } + + return future + } + } +} From 7b8e669eb9ac7247a218e607e36d54342e9d472e Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Mon, 31 Mar 2025 13:42:36 +1030 Subject: [PATCH 06/43] Android 15 fixes (#1066) * SES-3589 - qa tags * Catering for insets in android 15 * Fixing scrim colour * Adding back loader on images as it is useful for outgoing messages * Catering for keyboard insets due to new target sdk 35 * Using latest webrtc lib * Catering for insets when calculating recyclerview scroll * Enabling predictive back gesture for android 15 devices * Reworking new message fragment for ime handling on all versions including small screens * Removing insets from base app bar as theyare handled by the base activity --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 1 + .../securesms/BaseActionBarActivity.java | 133 ------------ .../securesms/BaseActionBarActivity.kt | 159 +++++++++++++++ .../securesms/components/ShapeScrim.java | 107 ---------- .../start/newmessage/NewMessage.kt | 193 +++++++++--------- .../conversation/v2/ConversationActivityV2.kt | 8 +- .../v2/ConversationReactionOverlay.kt | 2 +- .../v2/utilities/ThumbnailView.kt | 8 +- .../groups/EnterCommunityUrlFragment.kt | 3 +- .../loadaccount/LoadAccountActivity.kt | 5 + .../securesms/preferences/SettingsActivity.kt | 4 +- .../securesms/ui/components/AppBar.kt | 2 + .../securesms/util/GeneralUtilities.kt | 42 +++- .../res/layout/activity_conversation_v2.xml | 9 - .../layout/conversation_reaction_scrubber.xml | 2 +- app/src/main/res/layout/thumbnail_view.xml | 8 + app/src/main/res/values/attrs.xml | 9 - app/src/main/res/values/colors.xml | 5 +- .../src/main/res/values/strings.xml | 2 + libsession/src/main/res/values/attrs.xml | 9 - 21 files changed, 336 insertions(+), 377 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/ShapeScrim.java diff --git a/app/build.gradle b/app/build.gradle index f277becb2c2..dba4704e160 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -305,7 +305,7 @@ dependencies { implementation 'androidx.media3:media3-ui:1.4.0' implementation 'org.conscrypt:conscrypt-android:2.5.2' implementation 'org.signal:aesgcmprovider:0.0.3' - implementation 'io.github.webrtc-sdk:android:125.6422.06.1' + implementation 'io.github.webrtc-sdk:android:125.6422.07' implementation "me.leolin:ShortcutBadger:1.1.16" implementation 'org.apache.httpcomponents:httpclient-android:4.3.5' implementation 'com.github.chrisbanes:PhotoView:2.1.3' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 12a36085def..525ffe2c6d6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -81,6 +81,7 @@ android:largeHeap="true" android:networkSecurityConfig="@xml/network_security_configuration" android:supportsRtl="true" + android:enableOnBackInvokedCallback="true" android:theme="@style/Theme.Session.DayNight" tools:replace="android:allowBackup,android:label" > diff --git a/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java b/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java deleted file mode 100644 index 4e385cfe2b5..00000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java +++ /dev/null @@ -1,133 +0,0 @@ -package org.thoughtcrime.securesms; - -import static android.os.Build.VERSION.SDK_INT; -import static org.session.libsession.utilities.TextSecurePreferences.SELECTED_ACCENT_COLOR; - -import android.app.ActivityManager; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.os.Bundle; -import android.view.WindowManager; - -import androidx.annotation.Nullable; -import androidx.annotation.StyleRes; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; - -import org.session.libsession.utilities.TextSecurePreferences; -import org.thoughtcrime.securesms.conversation.v2.WindowUtil; -import org.thoughtcrime.securesms.util.ActivityUtilitiesKt; -import org.thoughtcrime.securesms.util.ThemeState; -import org.thoughtcrime.securesms.util.UiModeUtilities; - -import network.loki.messenger.R; - -public abstract class BaseActionBarActivity extends AppCompatActivity { - private static final String TAG = BaseActionBarActivity.class.getSimpleName(); - public ThemeState currentThemeState; - - private Resources.Theme modifiedTheme; - - private TextSecurePreferences getPreferences() { - ApplicationContext appContext = (ApplicationContext) getApplicationContext(); - return appContext.textSecurePreferences; - } - - @StyleRes - private int getDesiredTheme() { - ThemeState themeState = ActivityUtilitiesKt.themeState(getPreferences()); - int userSelectedTheme = themeState.getTheme(); - - // If the user has configured Session to follow the system light/dark theme mode then do so.. - if (themeState.getFollowSystem()) { - - // Use light or dark versions of the user's theme based on light-mode / dark-mode settings - boolean isDayUi = UiModeUtilities.isDayUiMode(this); - if (userSelectedTheme == R.style.Ocean_Dark || userSelectedTheme == R.style.Ocean_Light) { - return isDayUi ? R.style.Ocean_Light : R.style.Ocean_Dark; - } else { - return isDayUi ? R.style.Classic_Light : R.style.Classic_Dark; - } - } - else // ..otherwise just return their selected theme. - { - return userSelectedTheme; - } - } - - @StyleRes @Nullable - private Integer getAccentTheme() { - if (!getPreferences().hasPreference(SELECTED_ACCENT_COLOR)) return null; - ThemeState themeState = ActivityUtilitiesKt.themeState(getPreferences()); - return themeState.getAccentStyle(); - } - - @Override - public Resources.Theme getTheme() { - if (modifiedTheme != null) { - return modifiedTheme; - } - - // New themes - modifiedTheme = super.getTheme(); - modifiedTheme.applyStyle(getDesiredTheme(), true); - Integer accentTheme = getAccentTheme(); - if (accentTheme != null) { - modifiedTheme.applyStyle(accentTheme, true); - } - currentThemeState = ActivityUtilitiesKt.themeState(getPreferences()); - return modifiedTheme; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setHomeButtonEnabled(true); - } - } - - @Override - protected void onResume() { - super.onResume(); - initializeScreenshotSecurity(true); - String name = getResources().getString(R.string.app_name); - Bitmap icon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher_foreground); - int color = getResources().getColor(R.color.app_icon_background); - setTaskDescription(new ActivityManager.TaskDescription(name, icon, color)); - if (!currentThemeState.equals(ActivityUtilitiesKt.themeState(getPreferences()))) { - recreate(); - } - - // apply lightStatusBar manually as API 26 does not update properly via applyTheme - // https://issuetracker.google.com/issues/65883460?pli=1 - if (SDK_INT >= 26 && SDK_INT <= 27) WindowUtil.setLightStatusBarFromTheme(this); - if (SDK_INT == 27) WindowUtil.setLightNavigationBarFromTheme(this); - } - - @Override - protected void onPause() { - super.onPause(); - initializeScreenshotSecurity(false); - } - - @Override - public boolean onSupportNavigateUp() { - if (super.onSupportNavigateUp()) return true; - - onBackPressed(); - return true; - } - - private void initializeScreenshotSecurity(boolean isResume) { - if (!isResume) { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); - } else { - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.kt new file mode 100644 index 00000000000..717374eaf81 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.kt @@ -0,0 +1,159 @@ +package org.thoughtcrime.securesms + +import android.app.ActivityManager.TaskDescription +import android.content.res.Resources +import android.graphics.BitmapFactory +import android.os.Build.VERSION +import android.os.Bundle +import android.view.View +import android.view.WindowManager +import androidx.annotation.StyleRes +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import network.loki.messenger.R +import org.session.libsession.utilities.TextSecurePreferences +import org.thoughtcrime.securesms.conversation.v2.WindowUtil +import org.thoughtcrime.securesms.util.ThemeState +import org.thoughtcrime.securesms.util.UiModeUtilities.isDayUiMode +import org.thoughtcrime.securesms.util.themeState +import kotlin.math.max + +abstract class BaseActionBarActivity : AppCompatActivity() { + var currentThemeState: ThemeState? = null + + private var modifiedTheme: Resources.Theme? = null + + private val preferences: TextSecurePreferences + get() { + val appContext = + applicationContext as ApplicationContext + return appContext.textSecurePreferences + } + + @get:StyleRes + private val desiredTheme: Int + get() { + val themeState = preferences.themeState() + val userSelectedTheme = themeState.theme + + // If the user has configured Session to follow the system light/dark theme mode then do so.. + if (themeState.followSystem) { + // Use light or dark versions of the user's theme based on light-mode / dark-mode settings + + val isDayUi = isDayUiMode(this) + return if (userSelectedTheme == R.style.Ocean_Dark || userSelectedTheme == R.style.Ocean_Light) { + if (isDayUi) R.style.Ocean_Light else R.style.Ocean_Dark + } else { + if (isDayUi) R.style.Classic_Light else R.style.Classic_Dark + } + } else // ..otherwise just return their selected theme. + { + return userSelectedTheme + } + } + + @get:StyleRes + private val accentTheme: Int? + get() { + if (!preferences.hasPreference(TextSecurePreferences.SELECTED_ACCENT_COLOR)) return null + val themeState = preferences.themeState() + return themeState.accentStyle + } + + override fun getTheme(): Resources.Theme { + if (modifiedTheme != null) { + return modifiedTheme!! + } + + // New themes + modifiedTheme = super.getTheme() + modifiedTheme!!.applyStyle(desiredTheme, true) + val accentTheme = accentTheme + if (accentTheme != null) { + modifiedTheme!!.applyStyle(accentTheme, true) + } + currentThemeState = preferences.themeState() + return modifiedTheme!! + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Enable edge-to-edge - needed for sdk35 and above + WindowCompat.setDecorFitsSystemWindows(window, false) + + + val actionBar = supportActionBar + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true) + actionBar.setHomeButtonEnabled(true) + } + + // Apply insets to your views - Needed for sdk35 and above + val rootView = findViewById(android.R.id.content) + ViewCompat.setOnApplyWindowInsetsListener(rootView) { view, windowInsets -> + // Get system bars insets + val systemBarsInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + + // Get IME (keyboard) insets + val imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime()) + + // Update view padding to account for system bars + view.updatePadding( + left = systemBarsInsets.left, + top = systemBarsInsets.top, + right = systemBarsInsets.right, + bottom = max(systemBarsInsets.bottom, imeInsets.bottom) // set either the padding for the inset or for the keyboard + ) + + // Consume the insets + windowInsets + } + } + + override fun onResume() { + super.onResume() + initializeScreenshotSecurity(true) + val name = resources.getString(R.string.app_name) + val icon = BitmapFactory.decodeResource(resources, R.drawable.ic_launcher_foreground) + val color = resources.getColor(R.color.app_icon_background) + setTaskDescription(TaskDescription(name, icon, color)) + if (currentThemeState != preferences.themeState()) { + recreate() + } + + // apply lightStatusBar manually as API 26 does not update properly via applyTheme + // https://issuetracker.google.com/issues/65883460?pli=1 + if (VERSION.SDK_INT >= 26 && VERSION.SDK_INT <= 27) WindowUtil.setLightStatusBarFromTheme( + this + ) + if (VERSION.SDK_INT == 27) WindowUtil.setLightNavigationBarFromTheme(this) + } + + override fun onPause() { + super.onPause() + initializeScreenshotSecurity(false) + } + + override fun onSupportNavigateUp(): Boolean { + if (super.onSupportNavigateUp()) return true + + onBackPressed() + return true + } + + private fun initializeScreenshotSecurity(isResume: Boolean) { + if (!isResume) { + window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + } + + companion object { + private val TAG: String = BaseActionBarActivity::class.java.simpleName + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ShapeScrim.java b/app/src/main/java/org/thoughtcrime/securesms/components/ShapeScrim.java deleted file mode 100644 index b4239ecdd99..00000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ShapeScrim.java +++ /dev/null @@ -1,107 +0,0 @@ -package org.thoughtcrime.securesms.components; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffXfermode; -import android.graphics.RectF; -import android.util.AttributeSet; -import android.view.View; - -import network.loki.messenger.R; - -public class ShapeScrim extends View { - - private enum ShapeType { - CIRCLE, SQUARE - } - - private final Paint eraser; - private final ShapeType shape; - private final float radius; - - private Bitmap scrim; - private Canvas scrimCanvas; - - public ShapeScrim(Context context) { - this(context, null); - } - - public ShapeScrim(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public ShapeScrim(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - - if (attrs != null) { - TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ShapeScrim, 0, 0); - String shapeName = typedArray.getString(R.styleable.ShapeScrim_shape); - - if ("square".equalsIgnoreCase(shapeName)) this.shape = ShapeType.SQUARE; - else if ("circle".equalsIgnoreCase(shapeName)) this.shape = ShapeType.CIRCLE; - else this.shape = ShapeType.SQUARE; - - this.radius = typedArray.getFloat(R.styleable.ShapeScrim_radius, 0.4f); - - typedArray.recycle(); - } else { - this.shape = ShapeType.SQUARE; - this.radius = 0.4f; - } - - this.eraser = new Paint(); - this.eraser.setColor(0xFFFFFFFF); - this.eraser.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); - } - - @Override - public void onDraw(Canvas canvas) { - super.onDraw(canvas); - - int shortDimension = getWidth() < getHeight() ? getWidth() : getHeight(); - float drawRadius = shortDimension * radius; - - if (scrimCanvas == null) { - scrim = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); - scrimCanvas = new Canvas(scrim); - } - - scrim.eraseColor(Color.TRANSPARENT); - scrimCanvas.drawColor(Color.parseColor("#55BDBDBD")); - - if (shape == ShapeType.CIRCLE) drawCircle(scrimCanvas, drawRadius, eraser); - else drawSquare(scrimCanvas, drawRadius, eraser); - - canvas.drawBitmap(scrim, 0, 0, null); - } - - @Override - public void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { - super.onSizeChanged(width, height, oldHeight, oldHeight); - - if (width != oldWidth || height != oldHeight) { - scrim = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - scrimCanvas = new Canvas(scrim); - } - } - - private void drawCircle(Canvas canvas, float radius, Paint eraser) { - canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius, eraser); - } - - private void drawSquare(Canvas canvas, float radius, Paint eraser) { - float left = (getWidth() / 2 ) - radius; - float top = (getHeight() / 2) - radius; - float right = left + (radius * 2); - float bottom = top + (radius * 2); - - RectF square = new RectF(left, top, right, bottom); - - canvas.drawRoundRect(square, 25, 25, eraser); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt index 46e45045e19..22480a0965d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt @@ -1,16 +1,15 @@ package org.thoughtcrime.securesms.conversation.start.newmessage import android.graphics.Rect -import android.os.Build import android.view.ViewTreeObserver import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState @@ -18,32 +17,28 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import network.loki.messenger.R -import org.thoughtcrime.securesms.conversation.start.StartConversationFragment.Companion.PEEK_RATIO import org.thoughtcrime.securesms.ui.LoadingArcOr import org.thoughtcrime.securesms.ui.components.AppBarCloseIcon import org.thoughtcrime.securesms.ui.components.BackAppBar @@ -60,7 +55,6 @@ import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider import org.thoughtcrime.securesms.ui.theme.ThemeColors -import kotlin.math.max private val TITLES = listOf(R.string.accountIdEnter, R.string.qrScan) @@ -106,82 +100,83 @@ private fun EnterAccountId( callbacks: Callbacks, onHelp: () -> Unit = {} ) { - // the scaffold is required to provide the contentPadding. That contentPadding is needed - // to properly handle the ime padding. - Scaffold() { contentPadding -> - // we need this extra surface to handle nested scrolling properly, - // because this scrollable component is inside a bottomSheet dialog which is itself scrollable - Surface( - modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection()), - color = LocalColors.current.backgroundSecondary - ) { - - var accountModifier = Modifier - .fillMaxSize() + // Get accurate IME height + val keyboardHeight by keyboardHeightState() + val isKeyboardVisible = keyboardHeight > 0.dp + + // Use a Column as the main container + Column( + modifier = Modifier + .fillMaxSize() + .background(LocalColors.current.backgroundSecondary) + ) { + // Scrollable content area + Column( + modifier = Modifier + .weight(1f) .verticalScroll(rememberScrollState()) + .padding(vertical = LocalDimensions.current.spacing) + ) { + // Input field + SessionOutlinedTextField( + text = state.newMessageIdOrOns, + modifier = Modifier + .padding(horizontal = LocalDimensions.current.spacing) + .qaTag(stringResource(R.string.AccessibilityId_sessionIdInput)), + placeholder = stringResource(R.string.accountIdOrOnsEnter), + onChange = callbacks::onChange, + onContinue = callbacks::onContinue, + error = state.error?.string(), + isTextErrorColor = state.isTextErrorColor + ) + + Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing)) + + // Help button + BorderlessButtonWithIcon( + text = stringResource(R.string.messageNewDescriptionMobile), + modifier = Modifier + .contentDescription(R.string.AccessibilityId_messageNewDescriptionMobile) + .padding(horizontal = LocalDimensions.current.mediumSpacing) + .fillMaxWidth(), + style = LocalType.current.small, + color = LocalColors.current.textSecondary, + iconRes = R.drawable.ic_circle_help, + onClick = onHelp + ) + } - // There is a known issue with the ime padding on android versions below 30 - // So on these older versions we need to resort to some manual padding based on the visible height - // when the keyboard is up - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - val keyboardHeight by keyboardHeight() - accountModifier = accountModifier.padding(bottom = keyboardHeight) - } else { - accountModifier = accountModifier - .consumeWindowInsets(contentPadding) - .imePadding() - } - - Column( - modifier = accountModifier - ) { - Column( - modifier = Modifier.padding(vertical = LocalDimensions.current.spacing), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - SessionOutlinedTextField( - text = state.newMessageIdOrOns, - modifier = Modifier - .padding(horizontal = LocalDimensions.current.spacing) - .qaTag(stringResource(R.string.AccessibilityId_sessionIdInput)), - placeholder = stringResource(R.string.accountIdOrOnsEnter), - onChange = callbacks::onChange, - onContinue = callbacks::onContinue, - error = state.error?.string(), - isTextErrorColor = state.isTextErrorColor - ) - - Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing)) - - BorderlessButtonWithIcon( - text = stringResource(R.string.messageNewDescriptionMobile), - modifier = Modifier - .contentDescription(R.string.AccessibilityId_messageNewDescriptionMobile) - .padding(horizontal = LocalDimensions.current.mediumSpacing) - .fillMaxWidth(), - style = LocalType.current.small, - color = LocalColors.current.textSecondary, - iconRes = R.drawable.ic_circle_help, - onClick = onHelp - ) - } - - Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing)) - Spacer(Modifier.weight(2f)) - - PrimaryOutlineButton( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(horizontal = LocalDimensions.current.xlargeSpacing) - .padding(bottom = LocalDimensions.current.smallSpacing) - .fillMaxWidth() - .contentDescription(R.string.next), - enabled = state.isNextButtonEnabled, - onClick = callbacks::onContinue - ) { - LoadingArcOr(state.loading) { - Text(stringResource(R.string.next)) + // Add extra space at the bottom to prevent content from being hidden by the button + Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing)) + + // Button container that responds to keyboard visibility + Box( + modifier = Modifier + .fillMaxWidth() + .padding( + start = LocalDimensions.current.xlargeSpacing, + end = LocalDimensions.current.xlargeSpacing, + bottom = LocalDimensions.current.smallSpacing + ) + // Apply keyboard padding + .then( + if (isKeyboardVisible) { + Modifier.padding(bottom = keyboardHeight) + } else { + Modifier.navigationBarsPadding() } + ) + ) { + // Next button + PrimaryOutlineButton( + modifier = Modifier + .fillMaxWidth() + .contentDescription(R.string.next), + enabled = state.isNextButtonEnabled, + onClick = callbacks::onContinue + ) { + LoadingArcOr(state.loading) { + Text(stringResource(R.string.next)) } } } @@ -189,24 +184,38 @@ private fun EnterAccountId( } @Composable -fun keyboardHeight(): MutableState { +fun keyboardHeightState(): androidx.compose.runtime.State { val view = LocalView.current - var keyboardHeight = remember { mutableStateOf(0.dp) } + val keyboardHeight = remember { mutableStateOf(0.dp) } val density = LocalDensity.current + val context = LocalContext.current DisposableEffect(view) { + val rootView = view.rootView + val rect = Rect() + val listener = ViewTreeObserver.OnGlobalLayoutListener { - val rect = Rect() - view.getWindowVisibleDisplayFrame(rect) - val screenHeight = view.rootView.height * PEEK_RATIO - val keypadHeightPx = max( screenHeight - rect.bottom, 0f) + rootView.getWindowVisibleDisplayFrame(rect) + val screenHeight = rootView.height - keyboardHeight.value = with(density) { keypadHeightPx.toDp() } + // Get the system window insets to account for status bar, navigation bar, etc. + val windowInsets = ViewCompat.getRootWindowInsets(rootView) + val systemBarsBottom = windowInsets?.getInsets(WindowInsetsCompat.Type.systemBars())?.bottom ?: 0 + + // Calculate keyboard height taking into account the system bars + val keyboardHeightPx = screenHeight - rect.bottom - systemBarsBottom + + // Only consider as keyboard if height is significant + if (keyboardHeightPx > screenHeight * 0.15) { + keyboardHeight.value = with(density) { keyboardHeightPx.toDp() } + } else { + keyboardHeight.value = 0.dp + } } - view.viewTreeObserver.addOnGlobalLayoutListener(listener) + rootView.viewTreeObserver.addOnGlobalLayoutListener(listener) onDispose { - view.viewTreeObserver.removeOnGlobalLayoutListener(listener) + rootView.viewTreeObserver.removeOnGlobalLayoutListener(listener) } } 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 4b51ae7dc26..c2f7bd1b3fa 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 @@ -193,9 +193,11 @@ import org.thoughtcrime.securesms.util.SaveAttachmentTask import org.thoughtcrime.securesms.util.drawToBitmap import org.thoughtcrime.securesms.util.fadeIn import org.thoughtcrime.securesms.util.fadeOut +import org.thoughtcrime.securesms.util.isFullyScrolled import org.thoughtcrime.securesms.util.isScrolledToBottom import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom import org.thoughtcrime.securesms.util.push +import org.thoughtcrime.securesms.util.scrollAmount import org.thoughtcrime.securesms.util.show import org.thoughtcrime.securesms.util.toPx import org.thoughtcrime.securesms.webrtc.WebRtcCallActivity @@ -1540,16 +1542,12 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, } emojiPickerVisible = true ViewUtil.hideKeyboard(this, messageView) - binding.reactionsShade.isVisible = true binding.scrollToBottomButton.isVisible = false binding.conversationRecyclerView.suppressLayout(true) reactionDelegate.setOnActionSelectedListener(ReactionsToolbarListener(message)) reactionDelegate.setOnHideListener(object: ConversationReactionOverlay.OnHideListener { override fun startHide() { emojiPickerVisible = false - binding.reactionsShade.let { - ViewUtil.fadeOut(it, resources.getInteger(R.integer.reaction_scrubber_hide_duration), View.GONE) - } showScrollToBottomButtonIfApplicable() } @@ -2611,7 +2609,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, inner class ConversationAdapterDataObserver(val recyclerView: ConversationRecyclerView, val adapter: ConversationAdapter) : RecyclerView.AdapterDataObserver() { override fun onChanged() { super.onChanged() - if (recyclerView.isScrolledToWithin30dpOfBottom) { + if (recyclerView.isScrolledToWithin30dpOfBottom && !recyclerView.isFullyScrolled) { // Note: The adapter itemCount is zero based - so calling this with the itemCount in // a non-zero based manner scrolls us to the bottom of the last message (including // to the bottom of long messages as required by Jira SES-789 / GitHub 1364). diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt index e6694d002e8..8b0aa81a8b8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt @@ -333,7 +333,7 @@ class ConversationReactionOverlay : FrameLayout { private fun updateSystemUiOnShow(activity: Activity) { val window = activity.window - val barColor = ContextCompat.getColor(context, R.color.reactions_screen_dark_shade_color) + val barColor = ContextCompat.getColor(context, R.color.conversation_overlay_scrim) originalStatusBarColor = window.statusBarColor WindowUtil.setStatusBarColor(window, barColor) originalNavigationBarColor = window.navigationBarColor diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt index 8a5b894a01e..4d4e21d5f3d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.kt @@ -47,6 +47,8 @@ open class ThumbnailView @JvmOverloads constructor( private val dimensDelegate = ThumbnailDimensDelegate() + val loadIndicator: View by lazy { binding.thumbnailLoadIndicator } + private var slide: Slide? = null private val errorDrawable by lazy { @@ -144,6 +146,8 @@ open class ThumbnailView @JvmOverloads constructor( this.slide = slide + binding.thumbnailLoadIndicator.isVisible = slide.isInProgress + dimensDelegate.setDimens(naturalWidth, naturalHeight) invalidate() @@ -151,7 +155,7 @@ open class ThumbnailView @JvmOverloads constructor( when { slide.thumbnailUri != null -> { buildThumbnailGlideRequest(glide, slide).into( - GlideDrawableListeningTarget(binding.thumbnailImage, null, it) + GlideDrawableListeningTarget(binding.thumbnailImage, binding.thumbnailLoadIndicator, it) ) } slide.hasPlaceholder() -> { @@ -201,7 +205,7 @@ open class ThumbnailView @JvmOverloads constructor( private fun RequestBuilder.intoDrawableTargetAsFuture() = SettableFuture().also { binding.run { - GlideDrawableListeningTarget(thumbnailImage, null, it) + GlideDrawableListeningTarget(thumbnailImage, binding.thumbnailLoadIndicator, it) }.let { into(it) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/EnterCommunityUrlFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/EnterCommunityUrlFragment.kt index 26c9fd1fdd3..4e2852b2426 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/EnterCommunityUrlFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/EnterCommunityUrlFragment.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.groups +import android.content.Context.INPUT_METHOD_SERVICE import android.graphics.BitmapFactory import android.os.Bundle import android.view.LayoutInflater @@ -82,7 +83,7 @@ class EnterCommunityUrlFragment : Fragment() { // region Convenience private fun joinCommunityIfPossible() { - val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager + val inputMethodManager = requireContext().getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager inputMethodManager.hideSoftInputFromWindow(binding.communityUrlEditText.windowToken, 0) val communityUrl = binding.communityUrlEditText.text.trim().toString().lowercase(Locale.US) delegate?.handleCommunityUrlEntered(communityUrl) diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/loadaccount/LoadAccountActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/loadaccount/LoadAccountActivity.kt index 39b119e5b67..2e4da626fbb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/loadaccount/LoadAccountActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/loadaccount/LoadAccountActivity.kt @@ -1,9 +1,14 @@ package org.thoughtcrime.securesms.onboarding.loadaccount import android.os.Bundle +import android.view.View import androidx.activity.viewModels import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index fe6a28c16c7..8a169bfb51e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -601,7 +601,7 @@ class SettingsActivity : ScreenLockActionBarActivity() { @OptIn(ExperimentalMaterial3Api::class) @Composable - fun AvatarBottomSheet( + fun AvatarBottomSheet( showCamera: Boolean, onDismissRequest: () -> Unit, onGalleryPicked: () -> Unit, @@ -618,6 +618,7 @@ class SettingsActivity : ScreenLockActionBarActivity() { horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.spacing) ) { AvatarOption( + modifier = Modifier.qaTag(stringResource(R.string.AccessibilityId_imageButton)), title = stringResource(R.string.image), iconRes = R.drawable.ic_image, onClick = onGalleryPicked @@ -625,6 +626,7 @@ class SettingsActivity : ScreenLockActionBarActivity() { if(showCamera) { AvatarOption( + modifier = Modifier.qaTag(stringResource(R.string.AccessibilityId_cameraButton)), title = stringResource(R.string.contentDescriptionCamera), iconRes = R.drawable.ic_camera, onClick = onCameraPicked diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/AppBar.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/AppBar.kt index a9a1d55d963..4183c1fbc87 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/AppBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/AppBar.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -81,6 +82,7 @@ fun BasicAppBar( ) { CenterAlignedTopAppBar( modifier = modifier, + windowInsets = WindowInsets(0, 0, 0, 0), // insets handled in BaseActionBarActivity for now title = { AppBarText(title = title, singleLine = singleLine) }, diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/GeneralUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/GeneralUtilities.kt index cc40e0cc921..9fbac1ab90a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/GeneralUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/GeneralUtilities.kt @@ -1,6 +1,8 @@ package org.thoughtcrime.securesms.util import android.content.res.Resources +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import androidx.recyclerview.widget.RecyclerView import kotlin.math.roundToInt @@ -28,6 +30,40 @@ val RecyclerView.isScrolledToBottom: Boolean toPx(50, resources) >= computeVerticalScrollRange() val RecyclerView.isScrolledToWithin30dpOfBottom: Boolean - get() = computeVerticalScrollOffset().coerceAtLeast(0) + - computeVerticalScrollExtent() + - toPx(30, resources) >= computeVerticalScrollRange() \ No newline at end of file + get() { + // Retrieve the bottom inset from the window insets, if available. + val bottomInset = ViewCompat.getRootWindowInsets(this) + ?.getInsets(WindowInsetsCompat.Type.systemBars())?.bottom ?: 0 + + return computeVerticalScrollOffset().coerceAtLeast(0) + + computeVerticalScrollExtent() + + toPx(30, resources) + + bottomInset >= computeVerticalScrollRange() + } + + +val RecyclerView.isFullyScrolled: Boolean + get() { + val scrollOffset = computeVerticalScrollOffset().coerceAtLeast(0) + val scrollExtent = computeVerticalScrollExtent() + val scrollRange = computeVerticalScrollRange() + + /// Retrieve the bottom inset from the window insets, if available. + val bottomInset = ViewCompat.getRootWindowInsets(this) + ?.getInsets(WindowInsetsCompat.Type.systemBars())?.bottom ?: 0 + + // We're at the bottom if the offset + extent equals the range (accounting for insets) + return scrollOffset + scrollExtent >= scrollRange - bottomInset + } + +val RecyclerView.scrollAmount: Int + get() { + val scrollOffset = computeVerticalScrollOffset().coerceAtLeast(0) + val scrollExtent = computeVerticalScrollExtent() + val scrollRange = computeVerticalScrollRange() + + + // We're at the bottom if the offset + extent equals the range + return scrollOffset + scrollExtent - scrollRange + } + diff --git a/app/src/main/res/layout/activity_conversation_v2.xml b/app/src/main/res/layout/activity_conversation_v2.xml index 0fc84015240..f935a44b0fe 100644 --- a/app/src/main/res/layout/activity_conversation_v2.xml +++ b/app/src/main/res/layout/activity_conversation_v2.xml @@ -73,15 +73,6 @@ app:layout_constraintBottom_toBottomOf="parent" android:visibility="gone"/> - - diff --git a/app/src/main/res/layout/thumbnail_view.xml b/app/src/main/res/layout/thumbnail_view.xml index ce6575f7a33..f6c371e6639 100644 --- a/app/src/main/res/layout/thumbnail_view.xml +++ b/app/src/main/res/layout/thumbnail_view.xml @@ -17,6 +17,14 @@ android:scaleType="center" android:contentDescription="@string/AccessibilityId_mediaMessage" /> + + - - - - @@ -132,11 +128,6 @@ - - - - - diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index d2b67f279b1..b3227b24911 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -39,6 +39,8 @@ #dfffffff #90000000 + #df5e5e5e + #26ffffff #30ffffff #40ffffff @@ -62,9 +64,6 @@ @color/transparent_black_15 - @color/transparent_black_70 - #df5e5e5e - @color/core_grey_95 @color/core_grey_60 diff --git a/content-descriptions/src/main/res/values/strings.xml b/content-descriptions/src/main/res/values/strings.xml index f1c40f0aa95..45f28ca40c7 100644 --- a/content-descriptions/src/main/res/values/strings.xml +++ b/content-descriptions/src/main/res/values/strings.xml @@ -49,6 +49,8 @@ Blocked contacts Account ID + Image button + Camera button Clear all No pending message requests diff --git a/libsession/src/main/res/values/attrs.xml b/libsession/src/main/res/values/attrs.xml index 0cdcb5a5ac8..c378cf12be0 100644 --- a/libsession/src/main/res/values/attrs.xml +++ b/libsession/src/main/res/values/attrs.xml @@ -10,10 +10,6 @@ - - - - @@ -77,11 +73,6 @@ - - - - - From 87851447315f7d3238170f190800b408036773dd Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Mon, 31 Mar 2025 15:12:09 +1100 Subject: [PATCH 07/43] [SES-3368] - Fix image partial loading issue (#1046) --- .../thoughtcrime/securesms/ShareActivity.kt | 3 ++- .../attachments/DatabaseAttachmentProvider.kt | 9 ++------ .../securesms/audio/AudioRecorder.java | 19 +++++++++------- .../securesms/events/PartProgressEvent.java | 19 ---------------- .../securesms/giph/ui/GiphyActivity.java | 3 ++- .../securesms/mediasend/MediaSendActivity.kt | 4 ++-- .../securesms/mediasend/MediaSendFragment.kt | 13 +++++------ .../securesms/providers/BlobProvider.java | 20 ++++++++++++----- .../messaging/jobs/AttachmentUploadJob.kt | 4 ++-- .../attachments/SessionServiceAttachment.kt | 8 +------ .../SessionServiceAttachmentStream.kt | 4 ++-- .../utilities/ProfilePictureUtilities.kt | 1 - .../org/session/libsession/utilities/Util.kt | 15 +++++-------- .../messages/SignalServiceAttachment.java | 22 +------------------ .../SignalServiceAttachmentStream.java | 5 +---- .../streams/DigestingRequestBody.java | 11 +--------- .../utilities/PushAttachmentData.java | 9 +------- 17 files changed, 52 insertions(+), 117 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/events/PartProgressEvent.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/ShareActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/ShareActivity.kt index 3ba12796bbe..4a2a6c1b4ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ShareActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ShareActivity.kt @@ -297,7 +297,8 @@ class ShareActivity : ScreenLockActionBarActivity(), OnContactSelectedListener { .withMimeType(mimeType!!) .withFileName(fileName!!) .createForMultipleSessionsOnDisk(context, BlobProvider.ErrorListener { e: IOException? -> Log.w(TAG, "Failed to write to disk.", e) }) - } catch (ioe: IOException) { + .get() + } catch (ioe: Exception) { Log.w(TAG, ioe) return null } diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt index 05b3b176ea7..814116adfec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt @@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.attachments import android.content.Context import android.text.TextUtils import com.google.protobuf.ByteString -import org.greenrobot.eventbus.EventBus import org.session.libsession.database.MessageDataProvider import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.messages.MarkAsDeletedMessage @@ -30,7 +29,6 @@ import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.MessagingDatabase import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import org.thoughtcrime.securesms.events.PartProgressEvent import org.thoughtcrime.securesms.mms.MediaConstraints import org.thoughtcrime.securesms.mms.PartAuthority import org.thoughtcrime.securesms.util.MediaUtil @@ -345,7 +343,6 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) .withWidth(attachment.width) .withHeight(attachment.height) .withCaption(attachment.caption) - .withListener { total: Long, progress: Long -> EventBus.getDefault().postSticky(PartProgressEvent(attachment, total, progress)) } .build() } catch (ioe: IOException) { Log.w("Loki", "Couldn't open attachment", ioe) @@ -365,9 +362,8 @@ fun SessionServiceAttachmentPointer.toSignalPointer(): SignalServiceAttachmentPo fun DatabaseAttachment.toAttachmentStream(context: Context): SessionServiceAttachmentStream { val stream = PartAuthority.getAttachmentStream(context, this.dataUri!!) - val listener = SignalServiceAttachment.ProgressListener { total: Long, progress: Long -> EventBus.getDefault().postSticky(PartProgressEvent(this, total, progress))} - var attachmentStream = SessionServiceAttachmentStream(stream, this.contentType, this.size, this.filename, this.isVoiceNote, Optional.absent(), this.width, this.height, Optional.fromNullable(this.caption), listener) + var attachmentStream = SessionServiceAttachmentStream(stream, this.contentType, this.size, this.filename, this.isVoiceNote, Optional.absent(), this.width, this.height, Optional.fromNullable(this.caption)) attachmentStream.attachmentId = this.attachmentId.rowId attachmentStream.isAudio = MediaUtil.isAudio(this) attachmentStream.isGif = MediaUtil.isGif(this) @@ -409,9 +405,8 @@ fun DatabaseAttachment.toSignalAttachmentPointer(): SignalServiceAttachmentPoint fun DatabaseAttachment.toSignalAttachmentStream(context: Context): SignalServiceAttachmentStream { val stream = PartAuthority.getAttachmentStream(context, this.dataUri!!) - val listener = SignalServiceAttachment.ProgressListener { total: Long, progress: Long -> EventBus.getDefault().postSticky(PartProgressEvent(this, total, progress))} - return SignalServiceAttachmentStream(stream, this.contentType, this.size, this.filename, this.isVoiceNote, Optional.absent(), this.width, this.height, Optional.fromNullable(this.caption), listener) + return SignalServiceAttachmentStream(stream, this.contentType, this.size, this.filename, this.isVoiceNote, Optional.absent(), this.width, this.height, Optional.fromNullable(this.caption)) } fun DatabaseAttachment.shouldHaveImageSize(): Boolean { diff --git a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java index 373c76857b3..1eb663c3d67 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioRecorder.java @@ -6,7 +6,10 @@ import android.util.Pair; import androidx.annotation.NonNull; import java.io.IOException; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + import org.session.libsession.utilities.MediaTypes; import org.session.libsession.utilities.Util; import org.session.libsignal.utilities.ListenableFuture; @@ -25,7 +28,7 @@ public class AudioRecorder { private final Context context; private AudioCodec audioCodec; - private Uri captureUri; + private Future blobWritingTask; // Simple interface that allows us to provide a callback method to our `startRecording` method public interface AudioMessageRecordingFinishedCallback { @@ -49,7 +52,7 @@ public void startRecording(AudioMessageRecordingFinishedCallback callback) { ParcelFileDescriptor fds[] = ParcelFileDescriptor.createPipe(); - captureUri = BlobProvider.getInstance() + blobWritingTask = BlobProvider.getInstance() .forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0) .withMimeType(MediaTypes.AUDIO_AAC) .createForSingleSessionOnDisk(context, e -> Log.w(TAG, "Error during recording", e)); @@ -70,14 +73,14 @@ public void startRecording(AudioMessageRecordingFinishedCallback callback) { final SettableFuture> future = new SettableFuture<>(); executor.execute(() -> { - if (audioCodec == null) { + if (audioCodec == null || blobWritingTask == null) { sendToFuture(future, new IOException("MediaRecorder was never initialized successfully!")); return; } audioCodec.stop(); - try { + final Uri captureUri = blobWritingTask.get(); long size = 0L; // Only obtain the media size if the voice message was at least our minimum allowed // duration (bypassing this work prevents the audio recording mechanism from getting into @@ -86,13 +89,13 @@ public void startRecording(AudioMessageRecordingFinishedCallback callback) { size = MediaUtil.getMediaSize(context, captureUri); } sendToFuture(future, new Pair<>(captureUri, size)); - } catch (IOException ioe) { - Log.w(TAG, ioe); - sendToFuture(future, ioe); + } catch (IOException | ExecutionException | InterruptedException e) { + Log.w(TAG, e); + sendToFuture(future, e); } audioCodec = null; - captureUri = null; + blobWritingTask = null; }); return future; diff --git a/app/src/main/java/org/thoughtcrime/securesms/events/PartProgressEvent.java b/app/src/main/java/org/thoughtcrime/securesms/events/PartProgressEvent.java deleted file mode 100644 index 1be748f54bb..00000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/events/PartProgressEvent.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.thoughtcrime.securesms.events; - - -import androidx.annotation.NonNull; - -import org.session.libsession.messaging.sending_receiving.attachments.Attachment; - -public class PartProgressEvent { - - public final Attachment attachment; - public final long total; - public final long progress; - - public PartProgressEvent(@NonNull Attachment attachment, long total, long progress) { - this.attachment = attachment; - this.total = total; - this.progress = progress; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java index 14ed428371f..68257fb0a79 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java @@ -112,7 +112,8 @@ protected Uri doInBackground(Void... params) { return BlobProvider.getInstance() .forData(data) .withMimeType(MediaTypes.IMAGE_GIF) - .createForSingleSessionOnDisk(GiphyActivity.this, e -> Log.w(TAG, "Failed to write to disk.", e)); + .createForSingleSessionOnDisk(GiphyActivity.this, e -> Log.w(TAG, "Failed to write to disk.", e)) + .get(); } catch (InterruptedException | ExecutionException | IOException e) { Log.w(TAG, e); return null; diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt index 9dbceca531e..c5ab98cb2a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt @@ -244,7 +244,7 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme "Failed to write to disk.", e ) - } + }.get() return@run Media( uri, @@ -257,7 +257,7 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme Optional.of(Media.ALL_MEDIA_BUCKET_ID), Optional.absent() ) - } catch (e: IOException) { + } catch (e: Exception) { return@run null } }, { media: Media? -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.kt index 58dca20cd20..56f64a8ed51 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.kt @@ -20,6 +20,7 @@ import android.view.inputmethod.EditorInfo import android.widget.ImageButton import android.widget.TextView import android.widget.TextView.OnEditorActionListener +import androidx.core.os.BundleCompat import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager @@ -28,6 +29,7 @@ import androidx.viewpager.widget.ViewPager.SimpleOnPageChangeListener import com.bumptech.glide.Glide import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R +import org.session.libsession.utilities.Address import org.session.libsession.utilities.MediaTypes import org.session.libsession.utilities.TextSecurePreferences.Companion.isEnterSendsEnabled import org.session.libsession.utilities.Util.cancelRunnableOnMain @@ -370,6 +372,7 @@ class MediaSendFragment : Fragment(), OnGlobalLayoutListener, RailItemListener, e ) } + .get() val updated = Media( uri, @@ -385,14 +388,8 @@ class MediaSendFragment : Fragment(), OnGlobalLayoutListener, RailItemListener, updatedMedia.add(updated) renderTimer!!.split("item") - } catch (e: InterruptedException) { - Log.w(TAG, "Failed to render image. Using base image.") - updatedMedia.add(media) - } catch (e: ExecutionException) { - Log.w(TAG, "Failed to render image. Using base image.") - updatedMedia.add(media) - } catch (e: IOException) { - Log.w(TAG, "Failed to render image. Using base image.") + } catch (e: Exception) { + Log.w(TAG, "Failed to render image. Using base image.", e) updatedMedia.add(media) } } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/providers/BlobProvider.java b/app/src/main/java/org/thoughtcrime/securesms/providers/BlobProvider.java index 6d900a5b374..15240e4d3c8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/providers/BlobProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/providers/BlobProvider.java @@ -25,6 +25,10 @@ import java.util.HashMap; import java.util.Map; import java.util.UUID; +import java.util.concurrent.Future; + +import kotlin.Pair; +import kotlin.Result; /** * Allows for the creation and retrieval of blobs. @@ -173,23 +177,27 @@ public static boolean isAuthority(@NonNull Uri uri) { } @WorkerThread - private synchronized @NonNull Uri writeBlobSpecToDisk(@NonNull Context context, @NonNull BlobSpec blobSpec, @Nullable ErrorListener errorListener) throws IOException { + @NonNull + private static Future writeBlobSpecToDisk(@NonNull Context context, @NonNull BlobSpec blobSpec, @Nullable ErrorListener errorListener) throws IOException { AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(); String directory = getDirectory(blobSpec.getStorageType()); File outputFile = new File(getOrCreateCacheDirectory(context, directory), buildFileName(blobSpec.id)); OutputStream outputStream = ModernEncryptingPartOutputStream.createFor(attachmentSecret, outputFile, true).second; - SignalExecutors.UNBOUNDED.execute(() -> { + final Uri uri = buildUri(blobSpec); + + return SignalExecutors.UNBOUNDED.submit(() -> { try { Util.copy(blobSpec.getData(), outputStream); + return uri; } catch (IOException e) { if (errorListener != null) { errorListener.onError(e); } + + throw e; } }); - - return buildUri(blobSpec); } private synchronized @NonNull Uri writeBlobSpecToMemory(@NonNull BlobSpec blobSpec, @NonNull byte[] data) { @@ -258,7 +266,7 @@ protected BlobSpec buildBlobSpec(@NonNull StorageType storageType) { * period from one {@link Application#onCreate()} to the next. */ @WorkerThread - public Uri createForSingleSessionOnDisk(@NonNull Context context, @Nullable ErrorListener errorListener) throws IOException { + public Future createForSingleSessionOnDisk(@NonNull Context context, @Nullable ErrorListener errorListener) throws IOException { return writeBlobSpecToDisk(context, buildBlobSpec(StorageType.SINGLE_SESSION_DISK), errorListener); } @@ -267,7 +275,7 @@ public Uri createForSingleSessionOnDisk(@NonNull Context context, @Nullable Erro * eventually call {@link BlobProvider#delete(Context, Uri)} when the blob is no longer in use. */ @WorkerThread - public Uri createForMultipleSessionsOnDisk(@NonNull Context context, @Nullable ErrorListener errorListener) throws IOException { + public Future createForMultipleSessionsOnDisk(@NonNull Context context, @Nullable ErrorListener errorListener) throws IOException { return writeBlobSpecToDisk(context, buildBlobSpec(StorageType.MULTI_SESSION_DISK), errorListener); } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt index d1edfb240bd..561b1667396 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt @@ -96,9 +96,9 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess val outputStreamFactory = if (encrypt) AttachmentCipherOutputStreamFactory(key) else PlaintextOutputStreamFactory() // Create a digesting request body but immediately read it out to a buffer. Doing this makes // it easier to deal with inputStream and outputStreamFactory. - val pad = PushAttachmentData(attachment.contentType, inputStream, length, outputStreamFactory, attachment.listener) + val pad = PushAttachmentData(attachment.contentType, inputStream, length, outputStreamFactory) val contentType = "application/octet-stream" - val drb = DigestingRequestBody(pad.data, pad.outputStreamFactory, contentType, pad.dataSize, pad.listener) + val drb = DigestingRequestBody(pad.data, pad.outputStreamFactory, contentType, pad.dataSize) Log.d("Loki", "File size: ${length.toDouble() / 1000} kb.") val b = Buffer() drb.writeTo(b) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/SessionServiceAttachment.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/SessionServiceAttachment.kt index 1f493a0a867..698bf2dc8cd 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/SessionServiceAttachment.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/SessionServiceAttachment.kt @@ -34,7 +34,6 @@ abstract class SessionServiceAttachment protected constructor(val contentType: S private var contentType: String? = null private var filename: String = "PlaceholderFilename" private var length: Long = 0 - private var listener: SignalServiceAttachment.ProgressListener? = null private var voiceNote = false private var width = 0 private var height = 0 @@ -59,11 +58,6 @@ abstract class SessionServiceAttachment protected constructor(val contentType: S return this } - fun withListener(listener: SignalServiceAttachment.ProgressListener?): Builder { - this.listener = listener - return this - } - fun withVoiceNote(voiceNote: Boolean): Builder { this.voiceNote = voiceNote return this @@ -88,7 +82,7 @@ abstract class SessionServiceAttachment protected constructor(val contentType: S requireNotNull(inputStream) { "Must specify stream!" } requireNotNull(contentType) { "No content type specified!" } require(length != 0L) { "No length specified!" } - return SessionServiceAttachmentStream(inputStream, contentType, length, filename, voiceNote, Optional.absent(), width, height, Optional.fromNullable(caption), listener) + return SessionServiceAttachmentStream(inputStream, contentType, length, filename, voiceNote, Optional.absent(), width, height, Optional.fromNullable(caption)) } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/SessionServiceAttachmentStream.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/SessionServiceAttachmentStream.kt index 4e881e4b01c..4af1d16acc0 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/SessionServiceAttachmentStream.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/SessionServiceAttachmentStream.kt @@ -16,9 +16,9 @@ import kotlin.math.round /** * Represents a local SignalServiceAttachment to be sent. */ -class SessionServiceAttachmentStream(val inputStream: InputStream?, contentType: String?, val length: Long, val filename: String, val voiceNote: Boolean, val preview: Optional, val width: Int, val height: Int, val caption: Optional, val listener: SAttachment.ProgressListener?) : SessionServiceAttachment(contentType) { +class SessionServiceAttachmentStream(val inputStream: InputStream?, contentType: String?, val length: Long, val filename: String, val voiceNote: Boolean, val preview: Optional, val width: Int, val height: Int, val caption: Optional) : SessionServiceAttachment(contentType) { - constructor(inputStream: InputStream?, contentType: String?, length: Long, filename: String, voiceNote: Boolean, listener: SAttachment.ProgressListener?) : this(inputStream, contentType, length, filename, voiceNote, Optional.absent(), 0, 0, Optional.absent(), listener) {} + constructor(inputStream: InputStream?, contentType: String?, length: Long, filename: String, voiceNote: Boolean) : this(inputStream, contentType, length, filename, voiceNote, Optional.absent(), 0, 0, Optional.absent()) {} // Though now required, `digest` may be null for pre-existing records or from // messages received from other clients diff --git a/libsession/src/main/java/org/session/libsession/utilities/ProfilePictureUtilities.kt b/libsession/src/main/java/org/session/libsession/utilities/ProfilePictureUtilities.kt index c5e4e82d539..73c45d787ea 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/ProfilePictureUtilities.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/ProfilePictureUtilities.kt @@ -99,7 +99,6 @@ object ProfilePictureUtilities { pad.outputStreamFactory, pad.contentType, pad.dataLength, - null ) val b = Buffer() drb.writeTo(b) diff --git a/libsession/src/main/java/org/session/libsession/utilities/Util.kt b/libsession/src/main/java/org/session/libsession/utilities/Util.kt index e0c47c34c7d..58d6d4e0b6c 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/Util.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/Util.kt @@ -44,17 +44,12 @@ object Util { @JvmStatic @Throws(IOException::class) - fun copy(`in`: InputStream, out: OutputStream?): Long { - val buffer = ByteArray(8192) - var read: Int - var total: Long = 0 - while (`in`.read(buffer).also { read = it } != -1) { - out?.write(buffer, 0, read) - total += read.toLong() + fun copy(src: InputStream, dst: OutputStream): Long { + return src.use { + dst.use { + src.copyTo(dst) + } } - `in`.close() - out?.close() - return total } @JvmStatic diff --git a/libsignal/src/main/java/org/session/libsignal/messages/SignalServiceAttachment.java b/libsignal/src/main/java/org/session/libsignal/messages/SignalServiceAttachment.java index 07584a1c480..493135ccf03 100644 --- a/libsignal/src/main/java/org/session/libsignal/messages/SignalServiceAttachment.java +++ b/libsignal/src/main/java/org/session/libsignal/messages/SignalServiceAttachment.java @@ -43,7 +43,6 @@ public static class Builder { private String contentType; private String filename; private long length; - private ProgressListener listener; private boolean voiceNote; private int width; private int height; @@ -71,11 +70,6 @@ public Builder withFileName(String filename) { return this; } - public Builder withListener(ProgressListener listener) { - this.listener = listener; - return this; - } - public Builder withVoiceNote(boolean voiceNote) { this.voiceNote = voiceNote; return this; @@ -101,21 +95,7 @@ public SignalServiceAttachmentStream build() { if (contentType == null) throw new IllegalArgumentException("No content type specified!"); if (length == 0) throw new IllegalArgumentException("No length specified!"); - return new SignalServiceAttachmentStream(inputStream, contentType, length, filename, voiceNote, Optional.absent(), width, height, Optional.fromNullable(caption), listener); + return new SignalServiceAttachmentStream(inputStream, contentType, length, filename, voiceNote, Optional.absent(), width, height, Optional.fromNullable(caption)); } } - - /** - * An interface to receive progress information on upload/download of - * an attachment. - */ - public interface ProgressListener { - /** - * Called on a progress change event. - * - * @param total The total amount to transmit/receive in bytes. - * @param progress The amount that has been transmitted/received in bytes thus far - */ - public void onAttachmentProgress(long total, long progress); - } } diff --git a/libsignal/src/main/java/org/session/libsignal/messages/SignalServiceAttachmentStream.java b/libsignal/src/main/java/org/session/libsignal/messages/SignalServiceAttachmentStream.java index 63be579788d..062df319d3c 100644 --- a/libsignal/src/main/java/org/session/libsignal/messages/SignalServiceAttachmentStream.java +++ b/libsignal/src/main/java/org/session/libsignal/messages/SignalServiceAttachmentStream.java @@ -14,19 +14,17 @@ public class SignalServiceAttachmentStream extends SignalServiceAttachment { private final InputStream inputStream; private final long length; private final String filename; - private final ProgressListener listener; private final Optional preview; private final boolean voiceNote; private final int width; private final int height; private final Optional caption; - public SignalServiceAttachmentStream(InputStream inputStream, String contentType, long length, String filename, boolean voiceNote, Optional preview, int width, int height, Optional caption, ProgressListener listener) { + public SignalServiceAttachmentStream(InputStream inputStream, String contentType, long length, String filename, boolean voiceNote, Optional preview, int width, int height, Optional caption) { super(contentType); this.inputStream = inputStream; this.length = length; this.filename = filename; - this.listener = listener; this.voiceNote = voiceNote; this.preview = preview; this.width = width; @@ -43,7 +41,6 @@ public SignalServiceAttachmentStream(InputStream inputStream, String contentType public InputStream getInputStream() { return inputStream; } public long getLength() { return length; } public String getFilename() { return filename; } - public ProgressListener getListener() { return listener; } public Optional getPreview() { return preview; } public boolean getVoiceNote() { return voiceNote; } public int getWidth() { return width; } diff --git a/libsignal/src/main/java/org/session/libsignal/streams/DigestingRequestBody.java b/libsignal/src/main/java/org/session/libsignal/streams/DigestingRequestBody.java index 4eb739368a0..6e7d1f5514e 100644 --- a/libsignal/src/main/java/org/session/libsignal/streams/DigestingRequestBody.java +++ b/libsignal/src/main/java/org/session/libsignal/streams/DigestingRequestBody.java @@ -1,7 +1,5 @@ package org.session.libsignal.streams; -import org.session.libsignal.messages.SignalServiceAttachment.ProgressListener; - import java.io.IOException; import java.io.InputStream; @@ -15,20 +13,17 @@ public class DigestingRequestBody extends RequestBody { private final OutputStreamFactory outputStreamFactory; private final String contentType; private final long contentLength; - private final ProgressListener progressListener; private byte[] digest; public DigestingRequestBody(InputStream inputStream, OutputStreamFactory outputStreamFactory, - String contentType, long contentLength, - ProgressListener progressListener) + String contentType, long contentLength) { this.inputStream = inputStream; this.outputStreamFactory = outputStreamFactory; this.contentType = contentType; this.contentLength = contentLength; - this.progressListener = progressListener; } @Override @@ -47,10 +42,6 @@ public void writeTo(BufferedSink sink) throws IOException { while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) { outputStream.write(buffer, 0, read); total += read; - - if (progressListener != null) { - progressListener.onAttachmentProgress(contentLength, total); - } } outputStream.flush(); diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/PushAttachmentData.java b/libsignal/src/main/java/org/session/libsignal/utilities/PushAttachmentData.java index b0a6cd54d35..19d83f08faf 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/PushAttachmentData.java +++ b/libsignal/src/main/java/org/session/libsignal/utilities/PushAttachmentData.java @@ -6,7 +6,6 @@ package org.session.libsignal.utilities; -import org.session.libsignal.messages.SignalServiceAttachment.ProgressListener; import org.session.libsignal.streams.OutputStreamFactory; import java.io.InputStream; @@ -17,16 +16,14 @@ public class PushAttachmentData { private final InputStream data; private final long dataSize; private final OutputStreamFactory outputStreamFactory; - private final ProgressListener listener; public PushAttachmentData(String contentType, InputStream data, long dataSize, - OutputStreamFactory outputStreamFactory, ProgressListener listener) + OutputStreamFactory outputStreamFactory) { this.contentType = contentType; this.data = data; this.dataSize = dataSize; this.outputStreamFactory = outputStreamFactory; - this.listener = listener; } public String getContentType() { @@ -44,8 +41,4 @@ public long getDataSize() { public OutputStreamFactory getOutputStreamFactory() { return outputStreamFactory; } - - public ProgressListener getListener() { - return listener; - } } From b4a9b22529b731e39a7add718c9e5e703cd86333 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Tue, 1 Apr 2025 11:30:50 +1030 Subject: [PATCH 08/43] Fixing gradient issue on older android versions (#1069) paprently older versions of android can't use gradients with a mix of hex colors and dynamic theme attributes --- app/src/main/res/drawable/fade_gradient.xml | 2 +- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/themes.xml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/drawable/fade_gradient.xml b/app/src/main/res/drawable/fade_gradient.xml index 0a4c6bc9de1..c2a6a33eac6 100644 --- a/app/src/main/res/drawable/fade_gradient.xml +++ b/app/src/main/res/drawable/fade_gradient.xml @@ -5,7 +5,7 @@ diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 28811cb15db..52cc05d918b 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -23,6 +23,7 @@ + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index f8132745265..bd4b0b416d6 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -43,6 +43,7 @@ ?colorPrimary ?colorAccent ?danger + @color/transparent @style/MenuTextAppearance From 6aa0024aa98f1295d13a9c8b397b2f38c09ad386 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Tue, 1 Apr 2025 13:35:26 +1030 Subject: [PATCH 09/43] Searching for "Note to self" should show note to self in search results (#1070) --- .../org/thoughtcrime/securesms/home/HomeActivity.kt | 7 ++++++- .../securesms/home/search/GlobalSearchResult.kt | 3 ++- .../securesms/home/search/GlobalSearchViewModel.kt | 12 +++++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index d8d32cf5c8a..a5a67139129 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -269,7 +269,12 @@ class HomeActivity : ScreenLockActionBarActivity(), addAll(result.groupedContacts) } else -> buildList { - result.contactAndGroupList.takeUnless { it.isEmpty() }?.let { + val conversations = result.contactAndGroupList.toMutableList() + if(result.showNoteToSelf){ + conversations.add(GlobalSearchAdapter.Model.SavedMessages(publicKey)) + } + + conversations.takeUnless { it.isEmpty() }?.let { add(GlobalSearchAdapter.Model.Header(R.string.sessionConversations)) addAll(it) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchResult.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchResult.kt index 29e11067a0d..c2c5f01a205 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchResult.kt @@ -9,7 +9,8 @@ data class GlobalSearchResult( val query: String, val contacts: List = emptyList(), val threads: List = emptyList(), - val messages: List = emptyList() + val messages: List = emptyList(), + val showNoteToSelf: Boolean = false ) { val isEmpty: Boolean get() = contacts.isEmpty() && threads.isEmpty() && messages.isEmpty() diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt index dd94bf04d77..1afd54f92e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import kotlinx.coroutines.plus import kotlinx.coroutines.withContext +import network.loki.messenger.R import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.dependencies.ConfigFactory @@ -56,6 +57,8 @@ class GlobalSearchViewModel @Inject constructor( configFactory.configUpdateNotifications ) + val noteToSelfString by lazy { application.getString(R.string.noteToSelf).lowercase() } + val result = combine( _queryText, observeChangesAffectingSearch().onStart { emit(Unit) } @@ -73,7 +76,14 @@ class GlobalSearchViewModel @Inject constructor( ) } } else { - searchRepository.suspendQuery(query).toGlobalSearchResult() + val results = searchRepository.suspendQuery(query).toGlobalSearchResult() + + // show "Note to Self" is the user searches for parts of"Note to Self" + if(noteToSelfString.contains(query.lowercase())){ + results.copy(showNoteToSelf = true) + } else { + results + } } } catch (e: Exception) { Log.e("GlobalSearchViewModel", "Error searching len = ${query.length}", e) From f2cf7565e510479c5ede63e93e8e9df7d1839dd8 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Tue, 1 Apr 2025 15:47:00 +1030 Subject: [PATCH 10/43] Konvert searchRepository (#1071) --- .../securesms/search/SearchRepository.java | 285 ------------------ .../securesms/search/SearchRepository.kt | 262 ++++++++++++++++ 2 files changed, 262 insertions(+), 285 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java b/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java deleted file mode 100644 index 3da8f99c215..00000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java +++ /dev/null @@ -1,285 +0,0 @@ -package org.thoughtcrime.securesms.search; - -import android.content.Context; -import android.database.Cursor; -import android.database.DatabaseUtils; -import android.database.MergeCursor; -import androidx.annotation.NonNull; -import com.annimon.stream.Stream; -import org.session.libsession.messaging.contacts.Contact; -import org.session.libsession.utilities.Address; -import org.session.libsession.utilities.GroupRecord; -import org.session.libsession.utilities.TextSecurePreferences; -import org.session.libsession.utilities.recipients.Recipient; -import org.session.libsignal.utilities.Log; -import org.thoughtcrime.securesms.contacts.ContactAccessor; -import org.thoughtcrime.securesms.database.CursorList; -import org.thoughtcrime.securesms.database.GroupDatabase; -import org.thoughtcrime.securesms.database.MmsSmsColumns; -import org.thoughtcrime.securesms.database.SearchDatabase; -import org.thoughtcrime.securesms.database.SessionContactDatabase; -import org.thoughtcrime.securesms.database.ThreadDatabase; -import org.thoughtcrime.securesms.database.model.ThreadRecord; -import org.thoughtcrime.securesms.search.model.MessageResult; -import org.thoughtcrime.securesms.search.model.SearchResult; -import org.thoughtcrime.securesms.util.Stopwatch; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.Executor; -import kotlin.Pair; - -// Class to manage data retrieval for search -public class SearchRepository { - private static final String TAG = SearchRepository.class.getSimpleName(); - - private static final Set BANNED_CHARACTERS = new HashSet<>(); - static { - // Construct a list containing several ranges of invalid ASCII characters - // See: https://www.ascii-code.com/ - for (int i = 33; i <= 47; i++) { BANNED_CHARACTERS.add((char) i); } // !, ", #, $, %, &, ', (, ), *, +, ,, -, ., / - for (int i = 58; i <= 64; i++) { BANNED_CHARACTERS.add((char) i); } // :, ;, <, =, >, ?, @ - for (int i = 91; i <= 96; i++) { BANNED_CHARACTERS.add((char) i); } // [, \, ], ^, _, ` - for (int i = 123; i <= 126; i++) { BANNED_CHARACTERS.add((char) i); } // {, |, }, ~ - } - - private final Context context; - private final SearchDatabase searchDatabase; - private final ThreadDatabase threadDatabase; - private final GroupDatabase groupDatabase; - private final SessionContactDatabase contactDatabase; - private final ContactAccessor contactAccessor; - private final Executor executor; - - public SearchRepository(@NonNull Context context, - @NonNull SearchDatabase searchDatabase, - @NonNull ThreadDatabase threadDatabase, - @NonNull GroupDatabase groupDatabase, - @NonNull SessionContactDatabase contactDatabase, - @NonNull ContactAccessor contactAccessor, - @NonNull Executor executor) - { - this.context = context.getApplicationContext(); - this.searchDatabase = searchDatabase; - this.threadDatabase = threadDatabase; - this.groupDatabase = groupDatabase; - this.contactDatabase = contactDatabase; - this.contactAccessor = contactAccessor; - this.executor = executor; - } - - public void query(@NonNull String query, @NonNull Callback callback) { - // If the sanitized search is empty then abort without search - String cleanQuery = sanitizeQuery(query).trim(); - - executor.execute(() -> { - Stopwatch timer = new Stopwatch("FtsQuery"); - timer.split("clean"); - - Pair, List> contacts = queryContacts(cleanQuery); - timer.split("Contacts"); - - CursorList conversations = queryConversations(cleanQuery, contacts.getSecond()); - timer.split("Conversations"); - - CursorList messages = queryMessages(cleanQuery); - timer.split("Messages"); - - timer.stop(TAG); - - callback.onResult(new SearchResult(cleanQuery, contacts.getFirst(), conversations, messages)); - }); - } - - public void query(@NonNull String query, long threadId, @NonNull Callback> callback) { - // If the sanitized search query is empty then abort the search - String cleanQuery = sanitizeQuery(query).trim(); - if (cleanQuery.isEmpty()) { - callback.onResult(CursorList.emptyList()); - return; - } - - executor.execute(() -> { - CursorList messages = queryMessages(cleanQuery, threadId); - callback.onResult(messages); - }); - } - - public Pair, List> queryContacts(String query) { - Cursor contacts = contactDatabase.queryContactsByName(query); - List
contactList = new ArrayList<>(); - List contactStrings = new ArrayList<>(); - - while (contacts.moveToNext()) { - try { - Contact contact = contactDatabase.contactFromCursor(contacts); - String contactAccountId = contact.getAccountID(); - Address address = Address.fromSerialized(contactAccountId); - contactList.add(address); - contactStrings.add(contactAccountId); - } catch (Exception e) { - Log.e("Loki", "Error building Contact from cursor in query", e); - } - } - - contacts.close(); - - Cursor addressThreads = threadDatabase.searchConversationAddresses(query); - Cursor individualRecipients = threadDatabase.getFilteredConversationList(contactList); - if (individualRecipients == null && addressThreads == null) { - return new Pair<>(CursorList.emptyList(),contactStrings); - } - MergeCursor merged = new MergeCursor(new Cursor[]{addressThreads, individualRecipients}); - - return new Pair<>(new CursorList<>(merged, new ContactModelBuilder(contactDatabase, threadDatabase)), contactStrings); - } - - private CursorList queryConversations(@NonNull String query, List matchingAddresses) { - List numbers = contactAccessor.getNumbersForThreadSearchFilter(context, query); - String localUserNumber = TextSecurePreferences.getLocalNumber(context); - if (localUserNumber != null) { - matchingAddresses.remove(localUserNumber); - } - Set
addresses = new HashSet<>(Stream.of(numbers).map(number -> Address.fromExternal(context, number)).toList()); - - Cursor membersGroupList = groupDatabase.getGroupsFilteredByMembers(matchingAddresses); - if (membersGroupList != null) { - GroupDatabase.Reader reader = new GroupDatabase.Reader(membersGroupList); - while (membersGroupList.moveToNext()) { - GroupRecord record = reader.getCurrent(); - if (record == null) continue; - - addresses.add(Address.fromSerialized(record.getEncodedId())); - } - membersGroupList.close(); - } - - Cursor conversations = threadDatabase.getFilteredConversationList(new ArrayList<>(addresses)); - return conversations != null ? new CursorList<>(conversations, new GroupModelBuilder(threadDatabase, groupDatabase)) - : CursorList.emptyList(); - } - - private CursorList queryMessages(@NonNull String query) { - Cursor messages = searchDatabase.queryMessages(query); - return messages != null ? new CursorList<>(messages, new MessageModelBuilder(context)) - : CursorList.emptyList(); - } - - private CursorList queryMessages(@NonNull String query, long threadId) { - Cursor messages = searchDatabase.queryMessages(query, threadId); - return messages != null ? new CursorList<>(messages, new MessageModelBuilder(context)) - : CursorList.emptyList(); - } - - /** - * Unfortunately {@link DatabaseUtils#sqlEscapeString(String)} is not sufficient for our purposes. - * MATCH queries have a separate format of their own that disallow most "special" characters. - * - * Also, SQLite can't search for apostrophes, meaning we can't normally find words like "I'm". - * However, if we replace the apostrophe with a space, then the query will find the match. - */ - private String sanitizeQuery(@NonNull String query) { - StringBuilder out = new StringBuilder(); - - for (int i = 0; i < query.length(); i++) { - char c = query.charAt(i); - if (!BANNED_CHARACTERS.contains(c)) { - out.append(c); - } else if (c == '\'') { - out.append(' '); - } - } - - return out.toString(); - } - - private static class ContactModelBuilder implements CursorList.ModelBuilder { - - private final SessionContactDatabase contactDb; - private final ThreadDatabase threadDb; - - public ContactModelBuilder(SessionContactDatabase contactDb, ThreadDatabase threadDb) { - this.contactDb = contactDb; - this.threadDb = threadDb; - } - - @Override - public Contact build(@NonNull Cursor cursor) { - ThreadRecord threadRecord = threadDb.readerFor(cursor).getCurrent(); - Contact contact = contactDb.getContactWithAccountID(threadRecord.getRecipient().getAddress().toString()); - if (contact == null) { - contact = new Contact(threadRecord.getRecipient().getAddress().toString()); - contact.setThreadID(threadRecord.getThreadId()); - } - return contact; - } - } - - private static class RecipientModelBuilder implements CursorList.ModelBuilder { - - private final Context context; - - RecipientModelBuilder(@NonNull Context context) { this.context = context; } - - @Override - public Recipient build(@NonNull Cursor cursor) { - Address address = Address.fromExternal(context, cursor.getString(1)); - return Recipient.from(context, address, false); - } - } - - private static class GroupModelBuilder implements CursorList.ModelBuilder { - private final ThreadDatabase threadDatabase; - private final GroupDatabase groupDatabase; - - public GroupModelBuilder(ThreadDatabase threadDatabase, GroupDatabase groupDatabase) { - this.threadDatabase = threadDatabase; - this.groupDatabase = groupDatabase; - } - - @Override - public GroupRecord build(@NonNull Cursor cursor) { - ThreadRecord threadRecord = threadDatabase.readerFor(cursor).getCurrent(); - return groupDatabase.getGroup(threadRecord.getRecipient().getAddress().toGroupString()).get(); - } - } - - private static class ThreadModelBuilder implements CursorList.ModelBuilder { - - private final ThreadDatabase threadDatabase; - - ThreadModelBuilder(@NonNull ThreadDatabase threadDatabase) { - this.threadDatabase = threadDatabase; - } - - @Override - public ThreadRecord build(@NonNull Cursor cursor) { - return threadDatabase.readerFor(cursor).getCurrent(); - } - } - - private static class MessageModelBuilder implements CursorList.ModelBuilder { - - private final Context context; - - MessageModelBuilder(@NonNull Context context) { this.context = context; } - - @Override - public MessageResult build(@NonNull Cursor cursor) { - Address conversationAddress = Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.CONVERSATION_ADDRESS))); - Address messageAddress = Address.fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.MESSAGE_ADDRESS))); - Recipient conversationRecipient = Recipient.from(context, conversationAddress, false); - Recipient messageRecipient = Recipient.from(context, messageAddress, false); - String body = cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.SNIPPET)); - long sentMs = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_SENT)); - long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.THREAD_ID)); - - return new MessageResult(conversationRecipient, messageRecipient, body, threadId, sentMs); - } - } - - public interface Callback { - void onResult(@NonNull E result); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.kt new file mode 100644 index 00000000000..529bbec2478 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.kt @@ -0,0 +1,262 @@ +package org.thoughtcrime.securesms.search + +import android.content.Context +import android.database.Cursor +import android.database.MergeCursor +import com.annimon.stream.Stream +import org.session.libsession.messaging.contacts.Contact +import org.session.libsession.utilities.Address +import org.session.libsession.utilities.Address.Companion.fromExternal +import org.session.libsession.utilities.Address.Companion.fromSerialized +import org.session.libsession.utilities.GroupRecord +import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber +import org.session.libsession.utilities.recipients.Recipient +import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.contacts.ContactAccessor +import org.thoughtcrime.securesms.database.CursorList +import org.thoughtcrime.securesms.database.GroupDatabase +import org.thoughtcrime.securesms.database.MmsSmsColumns +import org.thoughtcrime.securesms.database.SearchDatabase +import org.thoughtcrime.securesms.database.SessionContactDatabase +import org.thoughtcrime.securesms.database.ThreadDatabase +import org.thoughtcrime.securesms.database.model.ThreadRecord +import org.thoughtcrime.securesms.search.model.MessageResult +import org.thoughtcrime.securesms.search.model.SearchResult +import org.thoughtcrime.securesms.util.Stopwatch +import java.util.concurrent.Executor + +// Class to manage data retrieval for search +class SearchRepository( + context: Context, + private val searchDatabase: SearchDatabase, + private val threadDatabase: ThreadDatabase, + private val groupDatabase: GroupDatabase, + private val contactDatabase: SessionContactDatabase, + private val contactAccessor: ContactAccessor, + private val executor: Executor +) { + private val context: Context = context.applicationContext + + fun query(query: String, callback: (SearchResult) -> Unit) { + // If the sanitized search is empty then abort without search + val cleanQuery = sanitizeQuery(query).trim { it <= ' ' } + + executor.execute { + val timer = + Stopwatch("FtsQuery") + timer.split("clean") + + val contacts = + queryContacts(cleanQuery) + timer.split("Contacts") + + val conversations = + queryConversations(cleanQuery, contacts.second) + timer.split("Conversations") + + val messages = queryMessages(cleanQuery) + timer.split("Messages") + + timer.stop(TAG) + callback( + SearchResult( + cleanQuery, + contacts.first, + conversations, + messages + ) + ) + } + } + + fun query(query: String, threadId: Long, callback: (CursorList) -> Unit) { + // If the sanitized search query is empty then abort the search + val cleanQuery = sanitizeQuery(query).trim { it <= ' ' } + if (cleanQuery.isEmpty()) { + callback(CursorList.emptyList()) + return + } + + executor.execute { + val messages = queryMessages(cleanQuery, threadId) + callback(messages) + } + } + + fun queryContacts(query: String): Pair, MutableList> { + val contacts = contactDatabase.queryContactsByName(query) + val contactList: MutableList
= ArrayList() + val contactStrings: MutableList = ArrayList() + + while (contacts.moveToNext()) { + try { + val contact = contactDatabase.contactFromCursor(contacts) + val contactAccountId = contact.accountID + val address = fromSerialized(contactAccountId) + contactList.add(address) + contactStrings.add(contactAccountId) + } catch (e: Exception) { + Log.e("Loki", "Error building Contact from cursor in query", e) + } + } + + contacts.close() + + val addressThreads = threadDatabase.searchConversationAddresses(query) + val individualRecipients = threadDatabase.getFilteredConversationList(contactList) + if (individualRecipients == null && addressThreads == null) { + return Pair(CursorList.emptyList(), contactStrings) + } + val merged = MergeCursor(arrayOf(addressThreads, individualRecipients)) + + return Pair( + CursorList(merged, ContactModelBuilder(contactDatabase, threadDatabase)), + contactStrings + ) + } + + private fun queryConversations( + query: String, + matchingAddresses: MutableList + ): CursorList { + val numbers = contactAccessor.getNumbersForThreadSearchFilter(context, query) + val localUserNumber = getLocalNumber(context) + if (localUserNumber != null) { + matchingAddresses.remove(localUserNumber) + } + val addresses: MutableSet
= HashSet(Stream.of(numbers).map { number: String? -> + fromExternal( + context, + number + ) + }.toList()) + + val membersGroupList = groupDatabase.getGroupsFilteredByMembers(matchingAddresses) + if (membersGroupList != null) { + val reader = GroupDatabase.Reader(membersGroupList) + while (membersGroupList.moveToNext()) { + val record = reader.current ?: continue + + addresses.add(fromSerialized(record.encodedId)) + } + membersGroupList.close() + } + + val conversations = threadDatabase.getFilteredConversationList(ArrayList(addresses)) + return if (conversations != null) + CursorList(conversations, GroupModelBuilder(threadDatabase, groupDatabase)) + else + CursorList.emptyList() + } + + private fun queryMessages(query: String): CursorList { + val messages = searchDatabase.queryMessages(query) + return if (messages != null) + CursorList(messages, MessageModelBuilder(context)) + else + CursorList.emptyList() + } + + private fun queryMessages(query: String, threadId: Long): CursorList { + val messages = searchDatabase.queryMessages(query, threadId) + return if (messages != null) + CursorList(messages, MessageModelBuilder(context)) + else + CursorList.emptyList() + } + + /** + * Unfortunately [DatabaseUtils.sqlEscapeString] is not sufficient for our purposes. + * MATCH queries have a separate format of their own that disallow most "special" characters. + * + * Also, SQLite can't search for apostrophes, meaning we can't normally find words like "I'm". + * However, if we replace the apostrophe with a space, then the query will find the match. + */ + private fun sanitizeQuery(query: String): String { + val out = StringBuilder() + + for (i in 0.. { + override fun build(cursor: Cursor): Contact { + val threadRecord = threadDb.readerFor(cursor).current + var contact = + contactDb.getContactWithAccountID(threadRecord.recipient.address.toString()) + if (contact == null) { + contact = Contact(threadRecord.recipient.address.toString()) + contact.threadID = threadRecord.threadId + } + return contact + } + } + + private class GroupModelBuilder( + private val threadDatabase: ThreadDatabase, + private val groupDatabase: GroupDatabase + ) : CursorList.ModelBuilder { + override fun build(cursor: Cursor): GroupRecord { + val threadRecord = threadDatabase.readerFor(cursor).current + return groupDatabase.getGroup(threadRecord.recipient.address.toGroupString()).get() + } + } + + private class MessageModelBuilder(private val context: Context) : CursorList.ModelBuilder { + override fun build(cursor: Cursor): MessageResult { + val conversationAddress = + fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.CONVERSATION_ADDRESS))) + val messageAddress = + fromSerialized(cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.MESSAGE_ADDRESS))) + val conversationRecipient = Recipient.from(context, conversationAddress, false) + val messageRecipient = Recipient.from(context, messageAddress, false) + val body = cursor.getString(cursor.getColumnIndexOrThrow(SearchDatabase.SNIPPET)) + val sentMs = + cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_SENT)) + val threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.THREAD_ID)) + + return MessageResult(conversationRecipient, messageRecipient, body, threadId, sentMs) + } + } + + interface Callback { + fun onResult(result: E) + } + + companion object { + private val TAG: String = SearchRepository::class.java.simpleName + + private val BANNED_CHARACTERS: MutableSet = HashSet() + + init { + // Construct a list containing several ranges of invalid ASCII characters + // See: https://www.ascii-code.com/ + for (i in 33..47) { + BANNED_CHARACTERS.add(i.toChar()) + } // !, ", #, $, %, &, ', (, ), *, +, ,, -, ., / + + for (i in 58..64) { + BANNED_CHARACTERS.add(i.toChar()) + } // :, ;, <, =, >, ?, @ + + for (i in 91..96) { + BANNED_CHARACTERS.add(i.toChar()) + } // [, \, ], ^, _, ` + + for (i in 123..126) { + BANNED_CHARACTERS.add(i.toChar()) + } // {, |, }, ~ + } + } +} From dbcffbd2b38fde15df8fb67561a043c1b11201f4 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Thu, 3 Apr 2025 16:16:16 +1100 Subject: [PATCH 11/43] Tidy up MediaSendFragment (#1068) --- .../conversation/v2/ConversationActivityV2.kt | 12 +- .../database/AttachmentDatabase.java | 27 +- .../mediapreview/MediaPreviewViewModel.java | 4 +- .../securesms/mediasend/Media.java | 95 ---- .../thoughtcrime/securesms/mediasend/Media.kt | 49 ++ .../securesms/mediasend/MediaRepository.java | 2 +- .../securesms/mediasend/MediaSendActivity.kt | 4 +- .../securesms/mediasend/MediaSendFragment.kt | 444 +++++++++--------- .../MediaSendFragmentPagerAdapter.java | 4 +- .../securesms/mediasend/MediaSendViewModel.kt | 6 +- .../securesms/providers/BlobProvider.java | 13 +- .../securesms/util/BitmapUtil.java | 6 +- 12 files changed, 319 insertions(+), 347 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediasend/Media.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediasend/Media.kt 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 c2f7bd1b3fa..58b08a5eec3 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 @@ -107,7 +107,6 @@ import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.ListenableFuture import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.guava.Optional import org.session.libsignal.utilities.hexEncodedPrivateKey import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ScreenLockActionBarActivity @@ -197,7 +196,6 @@ import org.thoughtcrime.securesms.util.isFullyScrolled import org.thoughtcrime.securesms.util.isScrolledToBottom import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom import org.thoughtcrime.securesms.util.push -import org.thoughtcrime.securesms.util.scrollAmount import org.thoughtcrime.securesms.util.show import org.thoughtcrime.securesms.util.toPx import org.thoughtcrime.securesms.webrtc.WebRtcCallActivity @@ -825,7 +823,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, AttachmentManager.MediaType.GIF == mediaType || AttachmentManager.MediaType.VIDEO == mediaType) ) { - val media = Media(mediaURI, filename, mimeType, 0, 0, 0, 0, Optional.absent(), Optional.absent()) + val media = Media(mediaURI, filename, mimeType, 0, 0, 0, 0, null, null) startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), viewModel.recipient!!, ""), PICK_FROM_LIBRARY) return } else { @@ -1908,7 +1906,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, val recipient = viewModel.recipient ?: return val mimeType = MediaUtil.getMimeType(this, contentUri)!! val filename = FilenameUtils.getFilenameFromUri(this, contentUri, mimeType) - val media = Media(contentUri, filename, mimeType, 0, 0, 0, 0, Optional.absent(), Optional.absent()) + val media = Media(contentUri, filename, mimeType, 0, 0, 0, 0, null, null) startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), recipient, getMessageBody()), PICK_FROM_LIBRARY) } @@ -2121,9 +2119,9 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, for (media in mediaList) { val mediaFilename: String? = media.filename when { - MediaUtil.isVideoType(media.mimeType) -> { slideDeck.addSlide(VideoSlide(this, media.uri, mediaFilename, 0, media.caption.orNull())) } - MediaUtil.isGif(media.mimeType) -> { slideDeck.addSlide(GifSlide(this, media.uri, mediaFilename, 0, media.width, media.height, media.caption.orNull())) } - MediaUtil.isImageType(media.mimeType) -> { slideDeck.addSlide(ImageSlide(this, media.uri, mediaFilename, 0, media.width, media.height, media.caption.orNull())) } + MediaUtil.isVideoType(media.mimeType) -> { slideDeck.addSlide(VideoSlide(this, media.uri, mediaFilename, 0, media.caption)) } + MediaUtil.isGif(media.mimeType) -> { slideDeck.addSlide(GifSlide(this, media.uri, mediaFilename, 0, media.width, media.height, media.caption)) } + MediaUtil.isImageType(media.mimeType) -> { slideDeck.addSlide(ImageSlide(this, media.uri, mediaFilename, 0, media.width, media.height, media.caption)) } else -> { Log.d(TAG, "Asked to send an unexpected media type: '" + media.mimeType + "'. Skipping.") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java index 5299885f7a9..4d474da0475 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -562,7 +562,9 @@ public void handleFailedAttachmentUpload(@NonNull AttachmentId id) { throw new MmsException("No attachment data found!"); } - dataInfo = setAttachmentData(dataInfo.file, mediaStream.getStream()); + final File oldFile = dataInfo.file; + + dataInfo = setAttachmentData(mediaStream.getStream()); ContentValues contentValues = new ContentValues(); contentValues.put(SIZE, dataInfo.length); @@ -570,9 +572,18 @@ public void handleFailedAttachmentUpload(@NonNull AttachmentId id) { contentValues.put(WIDTH, mediaStream.getWidth()); contentValues.put(HEIGHT, mediaStream.getHeight()); contentValues.put(DATA_RANDOM, dataInfo.random); + contentValues.put(DATA, dataInfo.file.getAbsolutePath()); database.update(TABLE_NAME, contentValues, PART_ID_WHERE, databaseAttachment.getAttachmentId().toStrings()); + if (oldFile != null && oldFile.exists()) { + try { + oldFile.delete(); + } catch (Exception e) { + Log.w(TAG, "Error deleting an old attachment file", e); + } + } + return new DatabaseAttachment(databaseAttachment.getAttachmentId(), databaseAttachment.getMmsId(), databaseAttachment.hasData(), @@ -696,20 +707,12 @@ public void setTransferState(long messageId, @NonNull AttachmentId attachmentId, try { File partsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE); File dataFile = File.createTempFile("part", ".mms", partsDirectory); - return setAttachmentData(dataFile, in); - } catch (IOException e) { - throw new MmsException(e); - } - } - private @NonNull DataInfo setAttachmentData(@NonNull File destination, @NonNull InputStream in) - throws MmsException - { - try { - Pair out = ModernEncryptingPartOutputStream.createFor(attachmentSecret, destination, false); + Log.d("AttachmentDatabase", "Writing attachment data to: " + dataFile.getAbsolutePath()); + Pair out = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false); long length = Util.copy(in, out.second); - return new DataInfo(destination, length, out.first); + return new DataInfo(dataFile, length, out.first); } catch (IOException e) { throw new MmsException(e); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java index 759a0b245c3..1f13efd396a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java @@ -119,8 +119,8 @@ private int getCursorPosition(int position) { mediaRecord.getAttachment().getWidth(), mediaRecord.getAttachment().getHeight(), mediaRecord.getAttachment().getSize(), - Optional.absent(), - Optional.fromNullable(mediaRecord.getAttachment().getCaption()) + null, + mediaRecord.getAttachment().getCaption() ); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/Media.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/Media.java deleted file mode 100644 index bd1e71decb5..00000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/Media.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.thoughtcrime.securesms.mediasend; - -import android.net.Uri; -import android.os.Parcel; -import android.os.Parcelable; -import androidx.annotation.NonNull; -import org.session.libsignal.utilities.guava.Optional; - -/** - * Represents a piece of media that the user has on their device. - */ -public class Media implements Parcelable { - - public static final String ALL_MEDIA_BUCKET_ID = "org.thoughtcrime.securesms.ALL_MEDIA"; - - private final Uri uri; - private final String filename; - private final String mimeType; - private final long date; - private final int width; - private final int height; - private final long size; - private final Optional bucketId; - private Optional caption; - - public Media(@NonNull Uri uri, @NonNull String filename, @NonNull String mimeType, long date, int width, int height, long size, Optional bucketId, Optional caption) { - this.uri = uri; - this.filename = filename; - this.mimeType = mimeType; - this.date = date; - this.width = width; - this.height = height; - this.size = size; - this.bucketId = bucketId; - this.caption = caption; - } - - protected Media(Parcel in) { - uri = in.readParcelable(Uri.class.getClassLoader()); - filename = in.readString(); - mimeType = in.readString(); - date = in.readLong(); - width = in.readInt(); - height = in.readInt(); - size = in.readLong(); - bucketId = Optional.fromNullable(in.readString()); - caption = Optional.fromNullable(in.readString()); - } - - public Uri getUri() { return uri; } - public String getFilename() { return filename; } - public String getMimeType() { return mimeType; } - public long getDate() { return date; } - public int getWidth() { return width; } - public int getHeight() { return height; } - public long getSize() { return size; } - public Optional getBucketId() { return bucketId; } - public Optional getCaption() { return caption; } - public void setCaption(String caption) { this.caption = Optional.fromNullable(caption); } - - @Override - public int describeContents() { return 0; } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeParcelable(uri, flags); - dest.writeString(filename); - dest.writeString(mimeType); - dest.writeLong(date); - dest.writeInt(width); - dest.writeInt(height); - dest.writeLong(size); - dest.writeString(bucketId.orNull()); - dest.writeString(caption.orNull()); - } - - public static final Creator CREATOR = new Creator() { - @Override - public Media createFromParcel(Parcel in) { return new Media(in); } - - @Override - public Media[] newArray(int size) { return new Media[size]; } - }; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Media media = (Media)o; - return uri.equals(media.uri); - } - - @Override - public int hashCode() { return uri.hashCode(); } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/Media.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/Media.kt new file mode 100644 index 00000000000..c72ced5375f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/Media.kt @@ -0,0 +1,49 @@ +package org.thoughtcrime.securesms.mediasend + +import android.net.Uri +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * Represents a piece of media that the user has on their device. + */ +@Parcelize +data class Media( + val uri: Uri, + val filename: String, + val mimeType: String, + val date: Long, + val width: Int, + val height: Int, + val size: Long, + val bucketId: String?, + val caption: String?, +) : Parcelable { + + // The equality check here is performed based only on the URI of the media. + // This behavior very opinionated and shouldn't really be in a generic equality check in the first place. + // However there are too much code working under this assumption and we can't simply change it to + // a generic solution. + // + // To later dev: once sufficient refactors are done, we can remove this equality + // check and rely on the data class default equality check instead. + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Media) return false + + if (uri != other.uri) return false + + return true + } + + override fun hashCode(): Int { + return uri.hashCode() + } + + + companion object { + const val ALL_MEDIA_BUCKET_ID: String = "org.thoughtcrime.securesms.ALL_MEDIA" + } + + +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java index bfe23f7d249..3b3c6ef8114 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java @@ -194,7 +194,7 @@ void getPopulatedMedia(@NonNull Context context, @NonNull List media, @No long size = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.SIZE)); String filename = cursor.getString(cursor.getColumnIndexOrThrow(Images.Media.DISPLAY_NAME)); - media.add(new Media(uri, filename, mimetype, date, width, height, size, Optional.of(bucketId), Optional.absent())); + media.add(new Media(uri, filename, mimetype, date, width, height, size, bucketId, null)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt index c5ab98cb2a6..2fc18762643 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt @@ -254,8 +254,8 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme width, height, data.size.toLong(), - Optional.of(Media.ALL_MEDIA_BUCKET_ID), - Optional.absent() + Media.ALL_MEDIA_BUCKET_ID, + null ) } catch (e: Exception) { return@run null diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.kt index 56f64a8ed51..efd46bd7e91 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.kt @@ -1,11 +1,9 @@ package org.thoughtcrime.securesms.mediasend -import android.annotation.SuppressLint import android.content.Context import android.graphics.Bitmap import android.graphics.Rect import android.net.Uri -import android.os.AsyncTask import android.os.Bundle import android.text.Editable import android.text.TextWatcher @@ -17,46 +15,39 @@ import android.view.ViewGroup import android.view.ViewTreeObserver.OnGlobalLayoutListener import android.view.WindowManager import android.view.inputmethod.EditorInfo -import android.widget.ImageButton import android.widget.TextView -import android.widget.TextView.OnEditorActionListener -import androidx.core.os.BundleCompat +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import androidx.viewpager.widget.ViewPager.SimpleOnPageChangeListener import com.bumptech.glide.Glide import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.withContext import network.loki.messenger.R -import org.session.libsession.utilities.Address +import network.loki.messenger.databinding.MediasendFragmentBinding import org.session.libsession.utilities.MediaTypes import org.session.libsession.utilities.TextSecurePreferences.Companion.isEnterSendsEnabled -import org.session.libsession.utilities.Util.cancelRunnableOnMain -import org.session.libsession.utilities.Util.isEmpty -import org.session.libsession.utilities.Util.runOnMainDelayed import org.session.libsession.utilities.recipients.Recipient -import org.session.libsignal.utilities.ListenableFuture import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.SettableFuture -import org.session.libsignal.utilities.guava.Optional -import org.thoughtcrime.securesms.components.ComposeText -import org.thoughtcrime.securesms.components.ControllableViewPager -import org.thoughtcrime.securesms.components.InputAwareLayout import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardHiddenListener import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener -import org.thoughtcrime.securesms.imageeditor.model.EditorModel import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter.RailItemListener -import org.thoughtcrime.securesms.mediasend.MediaSendViewModel import org.thoughtcrime.securesms.providers.BlobProvider import org.thoughtcrime.securesms.scribbles.ImageEditorFragment import org.thoughtcrime.securesms.util.PushCharacterCalculator -import org.thoughtcrime.securesms.util.Stopwatch -import java.io.ByteArrayOutputStream -import java.io.IOException +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream import java.util.Locale -import java.util.concurrent.ExecutionException /** * Allows the user to edit and caption a set of media items before choosing to send them. @@ -64,35 +55,24 @@ import java.util.concurrent.ExecutionException @AndroidEntryPoint class MediaSendFragment : Fragment(), OnGlobalLayoutListener, RailItemListener, OnKeyboardShownListener, OnKeyboardHiddenListener { - private var hud: InputAwareLayout? = null - private var captionAndRail: View? = null - private var sendButton: ImageButton? = null - private var composeText: ComposeText? = null - private var composeContainer: ViewGroup? = null - private var playbackControlsContainer: ViewGroup? = null - private var charactersLeft: TextView? = null - private var closeButton: View? = null - private var loader: View? = null - - private var fragmentPager: ControllableViewPager? = null + private var binding: MediasendFragmentBinding? = null + private var fragmentPagerAdapter: MediaSendFragmentPagerAdapter? = null - private var mediaRail: RecyclerView? = null private var mediaRailAdapter: MediaRailAdapter? = null private var visibleHeight = 0 private var viewModel: MediaSendViewModel? = null - private var controller: Controller? = null private val visibleBounds = Rect() private val characterCalculator = PushCharacterCalculator() + private val controller: Controller + get() = (parentFragment as? Controller) ?: requireActivity() as Controller + override fun onAttach(context: Context) { super.onAttach(context) - check(requireActivity() is Controller) { "Parent activity must implement controller interface." } - - controller = requireActivity() as Controller viewModel = ViewModelProvider(requireActivity()).get( MediaSendViewModel::class.java ) @@ -102,8 +82,8 @@ class MediaSendFragment : Fragment(), OnGlobalLayoutListener, RailItemListener, inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.mediasend_fragment, container, false) + ): View { + return MediasendFragmentBinding.inflate(inflater, container, false).root } override fun onCreate(savedInstanceState: Bundle?) { @@ -112,83 +92,76 @@ class MediaSendFragment : Fragment(), OnGlobalLayoutListener, RailItemListener, } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - hud = view.findViewById(R.id.mediasend_hud) - captionAndRail = view.findViewById(R.id.mediasend_caption_and_rail) - sendButton = view.findViewById(R.id.mediasend_send_button) - composeText = view.findViewById(R.id.mediasend_compose_text) - composeContainer = view.findViewById(R.id.mediasend_compose_container) - fragmentPager = view.findViewById(R.id.mediasend_pager) - mediaRail = view.findViewById(R.id.mediasend_media_rail) - playbackControlsContainer = view.findViewById(R.id.mediasend_playback_controls_container) - charactersLeft = view.findViewById(R.id.mediasend_characters_left) - closeButton = view.findViewById(R.id.mediasend_close_button) - loader = view.findViewById(R.id.loader) - - val sendButtonBkg = view.findViewById(R.id.mediasend_send_button_bkg) - - sendButton!!.setOnClickListener(View.OnClickListener { v: View? -> - if (hud!!.isKeyboardOpen()) { - hud!!.hideSoftkey(composeText, null) + val binding = MediasendFragmentBinding.bind(view).also { + this.binding = it + } + + binding.mediasendSendButton.setOnClickListener { v: View? -> + if (binding.mediasendHud.isKeyboardOpen) { + binding.mediasendHud.hideSoftkey(binding.mediasendComposeText, null) } - processMedia(fragmentPagerAdapter!!.allMedia, fragmentPagerAdapter!!.savedState) - }) + + fragmentPagerAdapter?.let { processMedia(it.allMedia, it.savedState) } + } val composeKeyPressedListener = ComposeKeyPressedListener() - composeText!!.setOnKeyListener(composeKeyPressedListener) - composeText!!.addTextChangedListener(composeKeyPressedListener) - composeText!!.setOnClickListener(composeKeyPressedListener) - composeText!!.setOnFocusChangeListener(composeKeyPressedListener) + binding.mediasendComposeText.setOnKeyListener(composeKeyPressedListener) + binding.mediasendComposeText.addTextChangedListener(composeKeyPressedListener) + binding.mediasendComposeText.setOnClickListener(composeKeyPressedListener) + binding.mediasendComposeText.setOnFocusChangeListener(composeKeyPressedListener) - composeText!!.requestFocus() + binding.mediasendComposeText.requestFocus() fragmentPagerAdapter = MediaSendFragmentPagerAdapter(childFragmentManager) - fragmentPager!!.setAdapter(fragmentPagerAdapter) + binding.mediasendPager.setAdapter(fragmentPagerAdapter) val pageChangeListener = FragmentPageChangeListener() - fragmentPager!!.addOnPageChangeListener(pageChangeListener) - fragmentPager!!.post(Runnable { pageChangeListener.onPageSelected(fragmentPager!!.currentItem) }) + binding.mediasendPager.addOnPageChangeListener(pageChangeListener) + binding.mediasendPager.post(Runnable { pageChangeListener.onPageSelected(binding.mediasendPager.currentItem) }) mediaRailAdapter = MediaRailAdapter(Glide.with(this), this, true) - mediaRail!!.setLayoutManager( + binding.mediasendMediaRail.setLayoutManager( LinearLayoutManager( requireContext(), LinearLayoutManager.HORIZONTAL, false ) ) - mediaRail!!.setAdapter(mediaRailAdapter) + binding.mediasendMediaRail.setAdapter(mediaRailAdapter) - hud!!.getRootView().viewTreeObserver.addOnGlobalLayoutListener(this) - hud!!.addOnKeyboardShownListener(this) - hud!!.addOnKeyboardHiddenListener(this) + binding.mediasendHud.getRootView().viewTreeObserver.addOnGlobalLayoutListener(this) + binding.mediasendHud.addOnKeyboardShownListener(this) + binding.mediasendHud.addOnKeyboardHiddenListener(this) - composeText!!.append(viewModel!!.body) + binding.mediasendComposeText.append(viewModel?.body) - val recipient = Recipient.from( - requireContext(), - arguments!!.getParcelable(KEY_ADDRESS)!!, false - ) - val displayName = Optional.fromNullable(recipient.name) - .or( - Optional.fromNullable(recipient.profileName) - .or(recipient.address.toString()) - ) - composeText!!.setHint(getString(R.string.message), null) - composeText!!.setOnEditorActionListener(OnEditorActionListener { v: TextView?, actionId: Int, event: KeyEvent? -> + binding.mediasendComposeText.setHint(getString(R.string.message), null) + binding.mediasendComposeText.setOnEditorActionListener { v: TextView?, actionId: Int, event: KeyEvent? -> val isSend = actionId == EditorInfo.IME_ACTION_SEND - if (isSend) sendButton!!.performClick() + if (isSend) binding.mediasendSendButton.performClick() isSend - }) + } - closeButton!!.setOnClickListener(View.OnClickListener { v: View? -> requireActivity().onBackPressed() }) + binding.mediasendCloseButton.setOnClickListener { requireActivity().onBackPressed() } + } + + override fun onDestroyView() { + super.onDestroyView() + + binding = null } override fun onStart() { super.onStart() - fragmentPagerAdapter!!.restoreState(viewModel!!.drawState) - viewModel!!.onImageEditorStarted() + val viewModel = viewModel + val adapter = fragmentPagerAdapter + + if (viewModel != null && adapter != null) { + adapter.restoreState(viewModel.drawState) + viewModel.onImageEditorStarted() + } requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN) @@ -200,216 +173,262 @@ class MediaSendFragment : Fragment(), OnGlobalLayoutListener, RailItemListener, override fun onStop() { super.onStop() - fragmentPagerAdapter!!.saveAllState() - viewModel!!.saveDrawState(fragmentPagerAdapter!!.savedState) + + val viewModel = viewModel + val adapter = fragmentPagerAdapter + + if (viewModel != null && adapter != null) { + adapter.saveAllState() + viewModel.saveDrawState(adapter.savedState) + } } override fun onGlobalLayout() { - hud!!.rootView.getWindowVisibleDisplayFrame(visibleBounds) + val hud = binding?.mediasendHud ?: return + + hud.rootView.getWindowVisibleDisplayFrame(visibleBounds) val currentVisibleHeight = visibleBounds.height() if (currentVisibleHeight != visibleHeight) { - hud!!.layoutParams.height = currentVisibleHeight - hud!!.layout( + hud.layoutParams.height = currentVisibleHeight + hud.layout( visibleBounds.left, visibleBounds.top, visibleBounds.right, visibleBounds.bottom ) - hud!!.requestLayout() + hud.requestLayout() visibleHeight = currentVisibleHeight } } override fun onRailItemClicked(distanceFromActive: Int) { - viewModel!!.onPageChanged(fragmentPager!!.currentItem + distanceFromActive) + val currentItem = binding?.mediasendPager?.currentItem ?: return + viewModel?.onPageChanged(currentItem + distanceFromActive) } override fun onRailItemDeleteClicked(distanceFromActive: Int) { - viewModel!!.onMediaItemRemoved( + val currentItem = binding?.mediasendPager?.currentItem ?: return + + viewModel?.onMediaItemRemoved( requireContext(), - fragmentPager!!.currentItem + distanceFromActive + currentItem + distanceFromActive ) } override fun onKeyboardShown() { - if (composeText!!.hasFocus()) { - mediaRail!!.visibility = View.VISIBLE - composeContainer!!.visibility = View.VISIBLE + val binding = binding ?: return + + if (binding.mediasendComposeText.hasFocus()) { + binding.mediasendMediaRail.visibility = View.VISIBLE + binding.mediasendComposeContainer.visibility = View.VISIBLE } else { - mediaRail!!.visibility = View.GONE - composeContainer!!.visibility = View.VISIBLE + binding.mediasendMediaRail.visibility = View.GONE + binding.mediasendComposeContainer.visibility = View.VISIBLE } } override fun onKeyboardHidden() { - composeContainer!!.visibility = View.VISIBLE - mediaRail!!.visibility = View.VISIBLE + binding?.apply { + mediasendComposeContainer.visibility = View.VISIBLE + mediasendMediaRail.visibility = View.VISIBLE + } } fun onTouchEventsNeeded(needed: Boolean) { - if (fragmentPager != null) { - fragmentPager!!.isEnabled = !needed - } + binding?.mediasendPager?.isEnabled = !needed } fun handleBackPress(): Boolean { - if (hud!!.isInputOpen) { - hud!!.hideCurrentInput(composeText) + val hud = binding?.mediasendHud ?: return false + val composeText = binding?.mediasendComposeText ?: return false + + if (hud.isInputOpen) { + hud.hideCurrentInput(composeText) return true } return false } private fun initViewModel() { - viewModel!!.getSelectedMedia().observe( + val viewModel = requireNotNull(viewModel) { + "ViewModel is not initialized" + } + + viewModel.getSelectedMedia().observe( this ) { media: List? -> - if (isEmpty(media)) { - controller!!.onNoMediaAvailable() + if (media.isNullOrEmpty()) { + controller.onNoMediaAvailable() return@observe } - fragmentPagerAdapter!!.setMedia(media!!) - mediaRail!!.visibility = View.VISIBLE - mediaRailAdapter!!.setMedia(media) + fragmentPagerAdapter?.setMedia(media) + + binding?.mediasendMediaRail?.visibility = View.VISIBLE + mediaRailAdapter?.setMedia(media) } - viewModel!!.getPosition().observe(this) { position: Int? -> + viewModel.getPosition().observe(this) { position: Int? -> if (position == null || position < 0) return@observe - fragmentPager!!.setCurrentItem(position, true) - mediaRailAdapter!!.setActivePosition(position) - mediaRail!!.smoothScrollToPosition(position) + binding?.mediasendPager?.setCurrentItem(position, true) + mediaRailAdapter?.setActivePosition(position) + binding?.mediasendMediaRail?.smoothScrollToPosition(position) - val playbackControls = fragmentPagerAdapter!!.getPlaybackControls(position) + val playbackControls = fragmentPagerAdapter?.getPlaybackControls(position) if (playbackControls != null) { val params = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) playbackControls.layoutParams = params - playbackControlsContainer!!.removeAllViews() - playbackControlsContainer!!.addView(playbackControls) + binding?.mediasendPlaybackControlsContainer?.removeAllViews() + binding?.mediasendPlaybackControlsContainer?.addView(playbackControls) } else { - playbackControlsContainer!!.removeAllViews() + binding?.mediasendPlaybackControlsContainer?.removeAllViews() } } - viewModel!!.getBucketId().observe(this) { bucketId: String? -> + viewModel.getBucketId().observe(this) { bucketId: String? -> if (bucketId == null) return@observe - mediaRailAdapter!!.setAddButtonListener { controller!!.onAddMediaClicked(bucketId) } + mediaRailAdapter!!.setAddButtonListener { controller.onAddMediaClicked(bucketId) } } } private fun presentCharactersRemaining() { - val messageBody = composeText!!.textTrimmed + val binding = binding ?: return + val messageBody = binding.mediasendComposeText.textTrimmed val characterState = characterCalculator.calculateCharacters(messageBody) if (characterState.charactersRemaining <= 15 || characterState.messagesSpent > 1) { - charactersLeft!!.text = String.format( + binding.mediasendCharactersLeft.text = String.format( Locale.getDefault(), "%d/%d (%d)", characterState.charactersRemaining, characterState.maxTotalMessageSize, characterState.messagesSpent ) - charactersLeft!!.visibility = View.VISIBLE + binding.mediasendCharactersLeft.visibility = View.VISIBLE } else { - charactersLeft!!.visibility = View.GONE + binding.mediasendCharactersLeft.visibility = View.GONE } } - @SuppressLint("StaticFieldLeak") private fun processMedia(mediaList: List, savedState: Map) { - val futures: MutableMap> = HashMap() + val binding = binding ?: return // If the view is destroyed, this process should not continue - for (media in mediaList) { - val state = savedState[media.uri] + val context = requireContext().applicationContext - if (state is ImageEditorFragment.Data) { - val model = state.readModel() - if (model != null && model.isChanged) { - futures[media] = render(requireContext(), model) - } - } - } - - object : AsyncTask>() { - private var renderTimer: Stopwatch? = null - private var progressTimer: Runnable? = null - - override fun onPreExecute() { - renderTimer = Stopwatch("ProcessMedia") - progressTimer = Runnable { - loader!!.visibility = View.VISIBLE - } - runOnMainDelayed(progressTimer!!, 250) + lifecycleScope.launch { + val delayedShowLoader = launch { + delay(250) + binding.loader.isVisible = true } - override fun doInBackground(vararg params: Void?): List { - val context = requireContext() - val updatedMedia: MutableList = ArrayList(mediaList.size) - - for (media in mediaList) { - if (futures.containsKey(media)) { - try { - val bitmap = futures[media]!!.get() - val baos = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos) - - val uri = BlobProvider.getInstance() - .forData(baos.toByteArray()) - .withMimeType(MediaTypes.IMAGE_JPEG) - .createForSingleSessionOnDisk( - context - ) { e: IOException? -> - Log.w( - TAG, - "Failed to write to disk.", - e - ) + val updatedMedia = supervisorScope { + // For each media, render the image in the background if necessary + val renderingTasks = mediaList + .asSequence() + .map { media -> + media to (savedState[media.uri] as? ImageEditorFragment.Data) + ?.readModel() + ?.takeIf { it.isChanged } + } + .associate { (media, model) -> + media.uri to async { + runCatching { + if (model != null) { + // While we render the bitmap in the background, make sure + // we limit the number of parallel tasks to avoid overwhelming the memory, + // as bitmaps are memory intensive. + withContext(Dispatchers.Default.limitedParallelism(2)) { + val bitmap = model.render(context) + try { + // Compress the bitmap to JPEG + val jpegOut = requireNotNull( + File.createTempFile( + "media_preview", + ".jpg", + context.cacheDir + ) + ) { + "Unable to create temporary file" + } + + val (jpegSize, uri) = try { + FileOutputStream(jpegOut).use { out -> + bitmap.compress( + Bitmap.CompressFormat.JPEG, + 80, + out + ) + } + + // Once we have the JPEG file, save it as our blob + val jpegSize = jpegOut.length() + jpegSize to BlobProvider.getInstance() + .forData(FileInputStream(jpegOut), jpegSize) + .withMimeType(MediaTypes.IMAGE_JPEG) + .withFileName(media.filename) + .createForSingleSessionOnDisk(context, null) + .await() + } finally { + // Clean up the temporary file + jpegOut.delete() + } + + media.copy( + uri = uri, + mimeType = MediaTypes.IMAGE_JPEG, + width = bitmap.width, + height = bitmap.height, + size = jpegSize, + ) + } finally { + bitmap.recycle() + } + } + } else { + // No changes to the original media, copy and return as is + val newUri = BlobProvider.getInstance() + .forData(requireNotNull(context.contentResolver.openInputStream(media.uri)) { + "Invalid URI" + }, media.size) + .withMimeType(media.mimeType) + .withFileName(media.filename) + .createForSingleSessionOnDisk(context, null) + .await() + + media.copy(uri = newUri) } - .get() - - val updated = Media( - uri, - media.filename, - MediaTypes.IMAGE_JPEG, - media.date, - bitmap.width, - bitmap.height, - baos.size().toLong(), - media.bucketId, - media.caption - ) - - updatedMedia.add(updated) - renderTimer!!.split("item") - } catch (e: Exception) { - Log.w(TAG, "Failed to render image. Using base image.", e) - updatedMedia.add(media) + } } - } else { - updatedMedia.add(media) } + + // For each media, if there's a rendered version, use that or keep the original + mediaList.map { media -> + renderingTasks[media.uri]?.await()?.let { rendered -> + if (rendered.isFailure) { + Log.w(TAG, "Error rendering image", rendered.exceptionOrNull()) + media + } else { + rendered.getOrThrow() + } + } ?: media } - return updatedMedia } - override fun onPostExecute(media: List) { - controller!!.onSendClicked(media, composeText!!.textTrimmed) - cancelRunnableOnMain(progressTimer!!) - loader!!.visibility = View.GONE - renderTimer!!.stop(TAG) - } - }.execute() + controller.onSendClicked(updatedMedia, binding.mediasendComposeText.textTrimmed) + delayedShowLoader.cancel() + binding.loader.isVisible = false + } } fun onRequestFullScreen(fullScreen: Boolean) { - captionAndRail!!.visibility = + binding?.mediasendCaptionAndRail?.visibility = if (fullScreen) View.GONE else View.VISIBLE } @@ -427,13 +446,13 @@ class MediaSendFragment : Fragment(), OnGlobalLayoutListener, RailItemListener, if (event.action == KeyEvent.ACTION_DOWN) { if (keyCode == KeyEvent.KEYCODE_ENTER) { if (isEnterSendsEnabled(requireContext())) { - sendButton!!.dispatchKeyEvent( + binding?.mediasendSendButton?.dispatchKeyEvent( KeyEvent( KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER ) ) - sendButton!!.dispatchKeyEvent( + binding?.mediasendSendButton?.dispatchKeyEvent( KeyEvent( KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER @@ -447,11 +466,12 @@ class MediaSendFragment : Fragment(), OnGlobalLayoutListener, RailItemListener, } override fun onClick(v: View) { - hud!!.showSoftkey(composeText) + val binding = binding ?: return + binding.mediasendHud.showSoftkey(binding.mediasendComposeText) } override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { - beforeLength = composeText!!.textTrimmed.length + beforeLength = binding?.mediasendComposeText?.textTrimmed?.length ?: return } override fun afterTextChanged(s: Editable) { @@ -483,13 +503,5 @@ class MediaSendFragment : Fragment(), OnGlobalLayoutListener, RailItemListener, fragment.arguments = args return fragment } - - private fun render(context: Context, model: EditorModel): ListenableFuture { - val future = SettableFuture() - - AsyncTask.THREAD_POOL_EXECUTOR.execute { future.set(model.render(context)) } - - return future - } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragmentPagerAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragmentPagerAdapter.java index 4d6107044fb..ea2ee1b4030 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragmentPagerAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragmentPagerAdapter.java @@ -17,6 +17,8 @@ import java.util.List; import java.util.Map; +import kotlin.collections.CollectionsKt; + class MediaSendFragmentPagerAdapter extends FragmentStatePagerAdapter { private final List media; @@ -83,7 +85,7 @@ public int getCount() { } List getAllMedia() { - return media; + return CollectionsKt.toList(media); } void setMedia(@NonNull List media) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.kt index c5150b29749..1831eb97aa1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.kt @@ -82,9 +82,9 @@ internal class MediaSendViewModel @Inject constructor( if (filteredMedia.size > 0) { val computedId: String = Stream.of(filteredMedia) .skip(1) - .reduce(filteredMedia.get(0).bucketId.or(Media.ALL_MEDIA_BUCKET_ID), + .reduce(filteredMedia.get(0).bucketId ?: Media.ALL_MEDIA_BUCKET_ID, { id: String?, m: Media -> - if (equals(id, m.bucketId.or(Media.ALL_MEDIA_BUCKET_ID))) { + if (equals(id, m.bucketId ?: Media.ALL_MEDIA_BUCKET_ID)) { return@reduce id } else { return@reduce Media.ALL_MEDIA_BUCKET_ID @@ -118,7 +118,7 @@ internal class MediaSendViewModel @Inject constructor( error.setValue(Error.ITEM_TOO_LARGE) bucketId.setValue(Media.ALL_MEDIA_BUCKET_ID) } else { - bucketId.setValue(filteredMedia.get(0).bucketId.or(Media.ALL_MEDIA_BUCKET_ID)) + bucketId.setValue(filteredMedia.get(0).bucketId ?: Media.ALL_MEDIA_BUCKET_ID) } countButtonVisibility = CountButtonState.Visibility.FORCED_OFF diff --git a/app/src/main/java/org/thoughtcrime/securesms/providers/BlobProvider.java b/app/src/main/java/org/thoughtcrime/securesms/providers/BlobProvider.java index 15240e4d3c8..8d05abd81eb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/providers/BlobProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/providers/BlobProvider.java @@ -25,6 +25,7 @@ import java.util.HashMap; import java.util.Map; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import kotlin.Pair; @@ -178,7 +179,7 @@ public static boolean isAuthority(@NonNull Uri uri) { @WorkerThread @NonNull - private static Future writeBlobSpecToDisk(@NonNull Context context, @NonNull BlobSpec blobSpec, @Nullable ErrorListener errorListener) throws IOException { + private static CompletableFuture writeBlobSpecToDisk(@NonNull Context context, @NonNull BlobSpec blobSpec, @Nullable ErrorListener errorListener) throws IOException { AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(); String directory = getDirectory(blobSpec.getStorageType()); File outputFile = new File(getOrCreateCacheDirectory(context, directory), buildFileName(blobSpec.id)); @@ -186,7 +187,7 @@ private static Future writeBlobSpecToDisk(@NonNull Context context, @NonNul final Uri uri = buildUri(blobSpec); - return SignalExecutors.UNBOUNDED.submit(() -> { + return CompletableFuture.supplyAsync(() -> { try { Util.copy(blobSpec.getData(), outputStream); return uri; @@ -195,9 +196,9 @@ private static Future writeBlobSpecToDisk(@NonNull Context context, @NonNul errorListener.onError(e); } - throw e; + throw new RuntimeException(e); } - }); + }, SignalExecutors.UNBOUNDED); } private synchronized @NonNull Uri writeBlobSpecToMemory(@NonNull BlobSpec blobSpec, @NonNull byte[] data) { @@ -266,7 +267,7 @@ protected BlobSpec buildBlobSpec(@NonNull StorageType storageType) { * period from one {@link Application#onCreate()} to the next. */ @WorkerThread - public Future createForSingleSessionOnDisk(@NonNull Context context, @Nullable ErrorListener errorListener) throws IOException { + public CompletableFuture createForSingleSessionOnDisk(@NonNull Context context, @Nullable ErrorListener errorListener) throws IOException { return writeBlobSpecToDisk(context, buildBlobSpec(StorageType.SINGLE_SESSION_DISK), errorListener); } @@ -275,7 +276,7 @@ public Future createForSingleSessionOnDisk(@NonNull Context context, @Nulla * eventually call {@link BlobProvider#delete(Context, Uri)} when the blob is no longer in use. */ @WorkerThread - public Future createForMultipleSessionsOnDisk(@NonNull Context context, @Nullable ErrorListener errorListener) throws IOException { + public CompletableFuture createForMultipleSessionsOnDisk(@NonNull Context context, @Nullable ErrorListener errorListener) throws IOException { return writeBlobSpecToDisk(context, buildBlobSpec(StorageType.MULTI_SESSION_DISK), errorListener); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/BitmapUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/BitmapUtil.java index 93b21512ea9..5e1a85df517 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/BitmapUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/BitmapUtil.java @@ -118,7 +118,9 @@ private static ScaleResult createScaledBytes(@NonNull Context context, do { totalAttempts++; ByteArrayOutputStream baos = new ByteArrayOutputStream(); - scaledBitmap.compress(format, quality, baos); + if (!scaledBitmap.compress(format, quality, baos)) { + Log.d(TAG, "Unable to compress image with quality " + quality); + } bytes = baos.toByteArray(); Log.d(TAG, "iteration with quality " + quality + " size " + bytes.length + " bytes."); @@ -144,7 +146,7 @@ private static ScaleResult createScaledBytes(@NonNull Context context, } } - if (bytes.length <= 0) { + if (bytes.length == 0) { throw new BitmapDecodingException("Decoding failed. Bitmap has a length of " + bytes.length + " bytes."); } From 04de99265c6f861a20e683a5af62ce77596e8efc Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Tue, 8 Apr 2025 13:24:58 +0930 Subject: [PATCH 12/43] Fix/ses 3518 qa fixes (#1075) * Window insets in app bars * Fixing broken draw for inputbar buttons (visible in light themes) * Fixing qa tag for legacy group recreation * SES-3597 - doc attachment corners * Fixing FAB animation * Extracting quotes and audio out of the message bubble. Ability to send text with documents (with no confirmation yet!) * SES-3603 Making sure quotes use underlying original text * Making sure we still render text when suppressing thumbnail * Reverting for now until we find a solution for confirming documents before sending * Cleaning up ui * Making sure items respect their bg shape --- .../conversation/v2/ConversationActivityV2.kt | 2 + .../conversation/v2/messages/QuoteView.kt | 21 ++++---- .../v2/messages/VisibleMessageContentView.kt | 43 ++++++++++------- .../securesms/home/HomeActivity.kt | 16 ++++++- .../securesms/preferences/SettingsActivity.kt | 20 ++++++-- .../thoughtcrime/securesms/ui/Components.kt | 33 ++++++++++++- .../securesms/ui/components/AppBar.kt | 1 + .../securesms/ui/theme/Dimensions.kt | 3 ++ .../thoughtcrime/securesms/ui/theme/Themes.kt | 11 +++-- .../thoughtcrime/securesms/util/GlowView.kt | 3 +- ...ived.xml => message_bubble_background.xml} | 0 .../view_doc_attachment_icon_background.xml | 10 ++++ .../res/layout/activity_conversation_v2.xml | 2 +- .../res/layout/view_attachment_control.xml | 2 +- .../view_conversation_typing_container.xml | 2 +- app/src/main/res/layout/view_document.xml | 6 ++- app/src/main/res/layout/view_quote.xml | 1 - .../layout/view_visible_message_content.xml | 48 +++++++++---------- .../main/res/layout/view_voice_message.xml | 1 + app/src/main/res/values-sw360dp/dimens.xml | 2 +- app/src/main/res/values-sw400dp/dimens.xml | 2 +- app/src/main/res/values/dimens.xml | 2 +- app/src/main/res/values/styles.xml | 2 +- .../src/main/res/values/strings.xml | 1 + .../messaging/messages/visible/Attachment.kt | 1 + .../NonTranslatableStringConstants.kt | 3 +- 26 files changed, 162 insertions(+), 76 deletions(-) rename app/src/main/res/drawable/{message_bubble_background_received.xml => message_bubble_background.xml} (100%) create mode 100644 app/src/main/res/drawable/view_doc_attachment_icon_background.xml 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 58b08a5eec3..6e070371b43 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 @@ -2086,6 +2086,8 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, // Note: The only multi-attachment message type is when sending images - all others // attempt send the attachment immediately upon file selection. sendAttachments(attachmentManager.buildSlideDeck().asAttachments(), null) + //todo: The current system sends the document the moment it has been selected, without text (body is set to null above) - We will want to fix this and allow the user to add text with a document AND be able to confirm before sending + //todo: Simply setting body to getMessageBody() above isn't good enough as it doesn't give the user a chance to confirm their message before sending it. } override fun onFailure(e: ExecutionException?) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt index bd3a88c9730..07a7d056234 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt @@ -9,6 +9,7 @@ import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.use import androidx.core.text.toSpannable import androidx.core.view.isVisible +import com.bumptech.glide.RequestManager import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R import network.loki.messenger.databinding.ViewQuoteBinding @@ -16,13 +17,11 @@ import org.session.libsession.messaging.contacts.Contact import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.getColorFromAttr import org.session.libsession.utilities.recipients.Recipient +import org.session.libsession.utilities.truncateIdForDisplay import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities import org.thoughtcrime.securesms.database.SessionContactDatabase -import com.bumptech.glide.RequestManager -import org.session.libsession.utilities.truncateIdForDisplay import org.thoughtcrime.securesms.mms.SlideDeck import org.thoughtcrime.securesms.util.MediaUtil -import org.thoughtcrime.securesms.util.getAccentColor import org.thoughtcrime.securesms.util.toPx import javax.inject.Inject @@ -106,25 +105,25 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet? attachments.audioSlide != null -> { val isVoiceNote = attachments.isVoiceNote if (isVoiceNote) { - binding.quoteViewBodyTextView.text = resources.getString(R.string.messageVoice) + updateQuoteTextIfEmpty(resources.getString(R.string.messageVoice)) binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_mic) } else { - binding.quoteViewBodyTextView.text = resources.getString(R.string.audio) + updateQuoteTextIfEmpty(resources.getString(R.string.audio)) binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_volume_2) } } attachments.documentSlide != null -> { binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_file) - binding.quoteViewBodyTextView.text = resources.getString(R.string.document) + updateQuoteTextIfEmpty(resources.getString(R.string.document)) } attachments.thumbnailSlide != null -> { val slide = attachments.thumbnailSlide!! if (MediaUtil.isVideo(slide.asAttachment())){ - binding.quoteViewBodyTextView.text = resources.getString(R.string.video) + updateQuoteTextIfEmpty(resources.getString(R.string.video)) binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_square_play) } else { - binding.quoteViewBodyTextView.text = resources.getString(R.string.image) + updateQuoteTextIfEmpty(resources.getString(R.string.image)) binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_image) } @@ -145,6 +144,12 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet? } } } + + private fun updateQuoteTextIfEmpty(text: String){ + if(binding.quoteViewBodyTextView.text.isNullOrEmpty()){ + binding.quoteViewBodyTextView.text = text + } + } // endregion // region Convenience diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index 61d81b037ce..8dcada7ec5f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.conversation.v2.messages import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent +import android.content.res.ColorStateList import android.graphics.Color import android.graphics.Rect import android.text.Spannable @@ -27,8 +28,6 @@ import com.bumptech.glide.RequestManager import network.loki.messenger.R import network.loki.messenger.databinding.ViewVisibleMessageContentBinding import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import org.session.libsession.messaging.jobs.AttachmentDownloadJob -import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.utilities.ThemeUtil @@ -81,6 +80,8 @@ class VisibleMessageContentView : ConstraintLayout { val color = if (message.isOutgoing) context.getAccentColor() else context.getColorFromAttr(R.attr.message_received_background_color) binding.contentParent.mainColor = color + binding.documentView.root.backgroundTintList = ColorStateList.valueOf(color) + binding.voiceMessageView.root.backgroundTintList = ColorStateList.valueOf(color) binding.contentParent.cornerRadius = resources.getDimension(R.dimen.message_corner_radius) val mediaDownloaded = message is MmsMessageRecord && message.slideDeck.asAttachments().all { it.isDone } @@ -269,24 +270,30 @@ class VisibleMessageContentView : ConstraintLayout { hideBody = false if (overallAttachmentState == AttachmentState.DONE || message.isOutgoing) { - if(suppressThumbnails) return // suppress thumbnail should hide the image, but we still want to show the attachment control if the state demands it + if(!suppressThumbnails) { // suppress thumbnail should hide the image, but we still want to show the attachment control if the state demands it - binding.attachmentControlView.root.isVisible = false + binding.attachmentControlView.root.isVisible = false - // isStart and isEnd of cluster needed for calculating the mask for full bubble image groups - // bind after add view because views are inflated and calculated during bind - binding.albumThumbnailView.root.isVisible = true - binding.albumThumbnailView.root.bind( - glideRequests = glide, - message = message, - isStart = isStartOfMessageCluster, - isEnd = isEndOfMessageCluster - ) - binding.albumThumbnailView.root.modifyLayoutParams { - horizontalBias = if (message.isOutgoing) 1f else 0f - } - onContentClick.add { event -> - binding.albumThumbnailView.root.calculateHitObject(event, message, thread, downloadPendingAttachment) + // isStart and isEnd of cluster needed for calculating the mask for full bubble image groups + // bind after add view because views are inflated and calculated during bind + binding.albumThumbnailView.root.isVisible = true + binding.albumThumbnailView.root.bind( + glideRequests = glide, + message = message, + isStart = isStartOfMessageCluster, + isEnd = isEndOfMessageCluster + ) + binding.albumThumbnailView.root.modifyLayoutParams { + horizontalBias = if (message.isOutgoing) 1f else 0f + } + onContentClick.add { event -> + binding.albumThumbnailView.root.calculateHitObject( + event, + message, + thread, + downloadPendingAttachment + ) + } } } else { databaseAttachments?.let { diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index a5a67139129..1afa12fceb4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -8,6 +8,7 @@ import android.content.Context import android.content.Intent import android.os.Build import android.os.Bundle +import android.view.View import android.widget.Toast import androidx.activity.viewModels import androidx.core.os.bundleOf @@ -389,8 +390,19 @@ class HomeActivity : ScreenLockActionBarActivity(), binding.sessionToolbar.isVisible = !isShown binding.recyclerView.isVisible = !isShown binding.seedReminderView.isVisible = !TextSecurePreferences.getHasViewedSeed(this) && !isShown - binding.globalSearchRecycler.isInvisible = !isShown - binding.conversationListContainer.isInvisible = isShown + binding.globalSearchRecycler.isVisible = isShown + binding.conversationListContainer.isVisible = !isShown + if(isShown){ + binding.newConversationButton.animate().cancel() + binding.newConversationButton.isVisible = false + } else { + binding.newConversationButton.apply { + alpha = 0.0f + visibility = View.VISIBLE + animate().cancel() + animate().setStartDelay(350).setDuration(250L).alpha(1.0f).setListener(null).start() + } + } } private fun updateLegacyConfigView() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index 8a169bfb51e..7cbfcffdaef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -36,6 +36,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState @@ -51,6 +52,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource @@ -99,6 +101,8 @@ import org.thoughtcrime.securesms.ui.components.BaseBottomSheet import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton import org.thoughtcrime.securesms.ui.components.PrimaryOutlineCopyButton import org.thoughtcrime.securesms.ui.contentDescription +import org.thoughtcrime.securesms.ui.getCellBottomShape +import org.thoughtcrime.securesms.ui.getCellTopShape import org.thoughtcrime.securesms.ui.qaTag import org.thoughtcrime.securesms.ui.setThemedContent import org.thoughtcrime.securesms.ui.theme.LocalColors @@ -494,12 +498,21 @@ class SettingsActivity : ScreenLockActionBarActivity() { Column { // add the debug menu in non release builds if (BuildConfig.BUILD_TYPE != "release") { - LargeItemButton("Debug Menu", R.drawable.ic_settings) { push() } + LargeItemButton( + "Debug Menu", + R.drawable.ic_settings, + shape = getCellTopShape() + ) { push() } Divider() } Crossfade(if (hasPaths) R.drawable.ic_status else R.drawable.ic_path_yellow, label = "path") { - LargeItemButtonWithDrawable(R.string.onionRoutingPath, it) { push() } + LargeItemButtonWithDrawable( + R.string.onionRoutingPath, + it, + shape = if (BuildConfig.BUILD_TYPE != "release") RectangleShape + else getCellTopShape() + ) { push() } } Divider() @@ -544,7 +557,8 @@ class SettingsActivity : ScreenLockActionBarActivity() { LargeItemButton(R.string.sessionClearData, R.drawable.ic_trash_2, Modifier.contentDescription(R.string.AccessibilityId_sessionClearData), - dangerButtonColors() + dangerButtonColors(), + shape = getCellBottomShape() ) { ClearAllDataDialog().show(supportFragmentManager, "Clear All Data Dialog") } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 86b8fc2c077..a9521c7492b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -47,6 +47,7 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.TileMode @@ -133,11 +134,12 @@ fun LargeItemButtonWithDrawable( @DrawableRes icon: Int, modifier: Modifier = Modifier, colors: ButtonColors = transparentButtonColors(), + shape: Shape = RectangleShape, onClick: () -> Unit ) { ItemButtonWithDrawable( textId, icon, modifier, - LocalType.current.h8, colors, onClick + LocalType.current.h8, colors, shape, onClick ) } @@ -148,6 +150,7 @@ fun ItemButtonWithDrawable( modifier: Modifier = Modifier, textStyle: TextStyle = LocalType.current.xl, colors: ButtonColors = transparentButtonColors(), + shape: Shape = RectangleShape, onClick: () -> Unit ) { val context = LocalContext.current @@ -164,6 +167,7 @@ fun ItemButtonWithDrawable( }, textStyle = textStyle, colors = colors, + shape = shape, onClick = onClick ) } @@ -174,6 +178,7 @@ fun LargeItemButton( @DrawableRes icon: Int, modifier: Modifier = Modifier, colors: ButtonColors = transparentButtonColors(), + shape: Shape = RectangleShape, onClick: () -> Unit ) { ItemButton( @@ -183,6 +188,7 @@ fun LargeItemButton( minHeight = LocalDimensions.current.minLargeItemButtonHeight, textStyle = LocalType.current.h8, colors = colors, + shape = shape, onClick = onClick ) } @@ -193,6 +199,7 @@ fun LargeItemButton( @DrawableRes icon: Int, modifier: Modifier = Modifier, colors: ButtonColors = transparentButtonColors(), + shape: Shape = RectangleShape, onClick: () -> Unit ) { ItemButton( @@ -202,6 +209,7 @@ fun LargeItemButton( minHeight = LocalDimensions.current.minLargeItemButtonHeight, textStyle = LocalType.current.h8, colors = colors, + shape = shape, onClick = onClick ) } @@ -214,6 +222,7 @@ fun ItemButton( minHeight: Dp = LocalDimensions.current.minItemButtonHeight, textStyle: TextStyle = LocalType.current.xl, colors: ButtonColors = transparentButtonColors(), + shape: Shape = RectangleShape, onClick: () -> Unit ) { ItemButton( @@ -229,6 +238,7 @@ fun ItemButton( minHeight = minHeight, textStyle = textStyle, colors = colors, + shape = shape, onClick = onClick ) } @@ -244,6 +254,7 @@ fun ItemButton( minHeight: Dp = LocalDimensions.current.minItemButtonHeight, textStyle: TextStyle = LocalType.current.xl, colors: ButtonColors = transparentButtonColors(), + shape: Shape = RectangleShape, onClick: () -> Unit ) { ItemButton( @@ -259,6 +270,7 @@ fun ItemButton( minHeight = minHeight, textStyle = textStyle, colors = colors, + shape = shape, onClick = onClick ) } @@ -276,6 +288,7 @@ fun ItemButton( minHeight: Dp = LocalDimensions.current.minLargeItemButtonHeight, textStyle: TextStyle = LocalType.current.xl, colors: ButtonColors = transparentButtonColors(), + shape: Shape = RectangleShape, onClick: () -> Unit ) { TextButton( @@ -283,7 +296,7 @@ fun ItemButton( colors = colors, onClick = onClick, contentPadding = PaddingValues(), - shape = RectangleShape, + shape = shape, ) { Box( modifier = Modifier @@ -345,6 +358,22 @@ fun Cell( } } +@Composable +fun getCellTopShape() = RoundedCornerShape( + topStart = LocalDimensions.current.shapeSmall, + topEnd = LocalDimensions.current.shapeSmall, + bottomEnd = 0.dp, + bottomStart = 0.dp +) + +@Composable +fun getCellBottomShape() = RoundedCornerShape( + topStart = 0.dp, + topEnd = 0.dp, + bottomEnd = LocalDimensions.current.shapeSmall, + bottomStart = LocalDimensions.current.shapeSmall +) + @Composable fun Modifier.contentDescription(text: GetString?): Modifier { return text?.let { diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/AppBar.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/AppBar.kt index 4183c1fbc87..941598d5301 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/AppBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/AppBar.kt @@ -134,6 +134,7 @@ fun ActionAppBar( ) { CenterAlignedTopAppBar( modifier = modifier, + windowInsets = WindowInsets(0, 0, 0, 0), // insets handled in BaseActionBarActivity for now title = { if (!actionMode) { AppBarText(title = title, singleLine = singleLine) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt index f342bc7d9e7..8798337ed52 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt @@ -29,4 +29,7 @@ data class Dimensions( val iconLarge: Dp = 46.dp, val iconXLarge: Dp = 60.dp, val iconXXLarge: Dp = 80.dp, + + val shapeSmall: Dp = 12.dp, + val shapeMedium: Dp = 16.dp, ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt index 9ef7c23da71..177512b6e7b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt @@ -47,7 +47,7 @@ fun SessionMaterialTheme( } /** - * Apply a given [ThemeColors], and our typography and shapes as a Material 2 Compose Theme. + * Apply a given [ThemeColors], and our typography and shapes as a Material 3 Compose Theme. **/ @Composable fun SessionMaterialTheme( @@ -57,7 +57,7 @@ fun SessionMaterialTheme( MaterialTheme( colorScheme = colors.toMaterialColors(), typography = sessionTypography.asMaterialTypography(), - shapes = sessionShapes, + shapes = sessionShapes(), ) { CompositionLocalProvider( LocalColors provides colors, @@ -72,9 +72,10 @@ fun SessionMaterialTheme( val pillShape = RoundedCornerShape(percent = 50) val buttonShape = pillShape -val sessionShapes = Shapes( - small = RoundedCornerShape(12.dp), - medium = RoundedCornerShape(16.dp) +@Composable +fun sessionShapes() = Shapes( + small = RoundedCornerShape(LocalDimensions.current.shapeSmall), + medium = RoundedCornerShape(LocalDimensions.current.shapeMedium) ) /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/GlowView.kt b/app/src/main/java/org/thoughtcrime/securesms/util/GlowView.kt index 76dd38ea144..0285857409e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/GlowView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/GlowView.kt @@ -198,7 +198,8 @@ class InputBarButtonImageViewContainer : RelativeLayout, GlowView { val h = height.toFloat() c.drawCircle(w / 2, h / 2, w / 2, fillPaint) if (strokeColor != 0) { - c.drawCircle(w / 2, h / 2, w / 2, strokePaint) + // Adjust radius to account for stroke width + c.drawCircle(w / 2, h / 2, w / 2 - strokePaint.strokeWidth / 2, strokePaint) } super.onDraw(c) } diff --git a/app/src/main/res/drawable/message_bubble_background_received.xml b/app/src/main/res/drawable/message_bubble_background.xml similarity index 100% rename from app/src/main/res/drawable/message_bubble_background_received.xml rename to app/src/main/res/drawable/message_bubble_background.xml diff --git a/app/src/main/res/drawable/view_doc_attachment_icon_background.xml b/app/src/main/res/drawable/view_doc_attachment_icon_background.xml new file mode 100644 index 00000000000..56b9f3007df --- /dev/null +++ b/app/src/main/res/drawable/view_doc_attachment_icon_background.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_conversation_v2.xml b/app/src/main/res/layout/activity_conversation_v2.xml index f935a44b0fe..eb75a92f87b 100644 --- a/app/src/main/res/layout/activity_conversation_v2.xml +++ b/app/src/main/res/layout/activity_conversation_v2.xml @@ -262,7 +262,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" style="@style/Widget.Session.Button.Common.ProminentOutline" - android:contentDescription="@string/AccessibilityId_messageRequestsAccept" + android:contentDescription="@string/AccessibilityId_recreate_legacy_group" android:text="@string/recreateGroup" /> diff --git a/app/src/main/res/layout/view_attachment_control.xml b/app/src/main/res/layout/view_attachment_control.xml index c9c4b2134dc..aebd079db60 100644 --- a/app/src/main/res/layout/view_attachment_control.xml +++ b/app/src/main/res/layout/view_attachment_control.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@drawable/message_bubble_background_received" + android:background="@drawable/message_bubble_background" android:contentDescription="@string/AccessibilityId_attachmentsClickToDownload" android:gravity="center" android:orientation="horizontal" diff --git a/app/src/main/res/layout/view_conversation_typing_container.xml b/app/src/main/res/layout/view_conversation_typing_container.xml index d2097f95c6f..f8bf25fbf6b 100644 --- a/app/src/main/res/layout/view_conversation_typing_container.xml +++ b/app/src/main/res/layout/view_conversation_typing_container.xml @@ -14,7 +14,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="7dp" - android:background="@drawable/message_bubble_background_received"> + android:background="@drawable/message_bubble_background"> + android:background="@drawable/view_doc_attachment_icon_background"> diff --git a/app/src/main/res/layout/view_visible_message_content.xml b/app/src/main/res/layout/view_visible_message_content.xml index c748148c8c9..2b653ae467c 100644 --- a/app/src/main/res/layout/view_visible_message_content.xml +++ b/app/src/main/res/layout/view_visible_message_content.xml @@ -56,14 +56,6 @@ android:layout_width="300dp" android:layout_height="wrap_content"/> - - - + - + - + diff --git a/app/src/main/res/values-sw360dp/dimens.xml b/app/src/main/res/values-sw360dp/dimens.xml index 485456ec3ee..2d3f2c3fb8d 100644 --- a/app/src/main/res/values-sw360dp/dimens.xml +++ b/app/src/main/res/values-sw360dp/dimens.xml @@ -7,5 +7,5 @@ 167dp 83dp 83dp - 240dp + 240dp \ No newline at end of file diff --git a/app/src/main/res/values-sw400dp/dimens.xml b/app/src/main/res/values-sw400dp/dimens.xml index 9376913d522..7dc4f477fd6 100644 --- a/app/src/main/res/values-sw400dp/dimens.xml +++ b/app/src/main/res/values-sw400dp/dimens.xml @@ -15,5 +15,5 @@ 199dp 99dp 99dp - 300dp + 300dp \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 0c57ec0680d..36aef5d6850 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -123,7 +123,7 @@ 320dp 40dp - 200dp + 200dp 34dp 26dp 26dp diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index f46ad041b40..7165580f2ac 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -243,7 +243,7 @@ - - - -