From bbb1b2b5170a59921167fae04790d34d123225a8 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 4 Jul 2024 17:50:17 +1000 Subject: [PATCH 01/10] Fix issue with span being the full length (#1528) --- .../securesms/util/RoundedBackgroundSpan.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/RoundedBackgroundSpan.kt b/app/src/main/java/org/thoughtcrime/securesms/util/RoundedBackgroundSpan.kt index ebefc9c50c..a4cf1b1e96 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/RoundedBackgroundSpan.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/RoundedBackgroundSpan.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.util import android.content.Context -import android.content.res.Resources import android.graphics.Canvas import android.graphics.Paint import android.graphics.RectF @@ -50,6 +49,17 @@ class RoundedBackgroundSpan( override fun getSize( paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt? ): Int { + // If the span covers the whole text, and the height is not set, draw() will not be called for the span. + // To help with that we need to take the font metric into account + val metrics = paint.fontMetricsInt + if (fm != null) { + fm.top = metrics.top + fm.ascent = metrics.ascent + fm.descent = metrics.descent + + fm.bottom = metrics.bottom + } + return (paint.measureText(text, start, end) + 2 * paddingHorizontal).toInt() } From 15b3b18321211a0d03e90d90362b31ba0683d467 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 5 Jul 2024 10:48:51 +1000 Subject: [PATCH 02/10] Proper display of unresolved names in mentions (#1530) * Fix issue with span being the full length * Making sure a mention with a username without a resolved name still displayed with the appropriate style with the truncated is --- .../securesms/conversation/v2/utilities/MentionUtilities.kt | 3 ++- .../src/main/java/org/session/libsession/utilities/IdUtil.kt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt index 3edfcffc97..3a4bfd6816 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt @@ -18,6 +18,7 @@ import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.getColorFromAttr +import org.session.libsession.utilities.truncateIdForDisplay import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.util.RoundedBackgroundSpan import org.thoughtcrime.securesms.util.getAccentColor @@ -67,7 +68,7 @@ object MentionUtilities { } else { val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey) @Suppress("NAME_SHADOWING") val context = if (openGroup != null) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR - contact?.displayName(context) + contact?.displayName(context) ?: truncateIdForDisplay(publicKey) } if (userDisplayName != null) { val mention = "@$userDisplayName" diff --git a/libsession/src/main/java/org/session/libsession/utilities/IdUtil.kt b/libsession/src/main/java/org/session/libsession/utilities/IdUtil.kt index ccaa31c2f9..1902b6e0ab 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/IdUtil.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/IdUtil.kt @@ -1,4 +1,4 @@ package org.session.libsession.utilities fun truncateIdForDisplay(id: String): String = - id.takeIf { it.length > 8 }?.apply{ "${take(4)}…${takeLast(4)}" } ?: id + id.takeIf { it.length > 8 }?.run{ "${take(4)}…${takeLast(4)}" } ?: id From e1f1372e17e57f4a00e95679cef41a3540a55408 Mon Sep 17 00:00:00 2001 From: Fanchao Liu <273191+simophin@users.noreply.github.com> Date: Mon, 8 Jul 2024 10:34:13 +1000 Subject: [PATCH 03/10] Testnet build (#1532) Co-authored-by: fanchao --- .../src/main/java/org/session/libsession/snode/SnodeAPI.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt index d2cfa2de35..146630b7dd 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -86,7 +86,7 @@ object SnodeAPI { private const val snodeFailureThreshold = 3 private const val useOnionRequests = true - const val useTestnet = false + const val useTestnet = true // Error internal sealed class Error(val description: String) : Exception(description) { From 075341abe199c2f7a65d5234467259c9156a025d Mon Sep 17 00:00:00 2001 From: Fanchao Liu <273191+simophin@users.noreply.github.com> Date: Mon, 8 Jul 2024 11:24:37 +1000 Subject: [PATCH 04/10] Allow "public.loki.foundation" to be accessed by http (#1534) Co-authored-by: fanchao --- app/src/main/res/xml/network_security_configuration.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/xml/network_security_configuration.xml b/app/src/main/res/xml/network_security_configuration.xml index f3a7419b55..7469ebe106 100644 --- a/app/src/main/res/xml/network_security_configuration.xml +++ b/app/src/main/res/xml/network_security_configuration.xml @@ -2,6 +2,7 @@ 127.0.0.1 + public.loki.foundation seed1.getsession.org From 2b27b7d5e0ed0c2593b68b272dbf7d37bc7c07ef Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Tue, 9 Jul 2024 09:33:06 +1000 Subject: [PATCH 05/10] Bumping the version code and name --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index aa3ad4e779..2e52adddd5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,8 +31,8 @@ configurations.all { exclude module: "commons-logging" } -def canonicalVersionCode = 373 -def canonicalVersionName = "1.18.4" +def canonicalVersionCode = 374 +def canonicalVersionName = "1.18.5" def postFixSize = 10 def abiPostFix = ['armeabi-v7a' : 1, From 0300be25f6c63efa799011c85bd290084f0af03d Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Tue, 9 Jul 2024 09:58:49 +1000 Subject: [PATCH 06/10] Reverting temporary change --- .../src/main/java/org/session/libsession/snode/SnodeAPI.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt index 146630b7dd..d2cfa2de35 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -86,7 +86,7 @@ object SnodeAPI { private const val snodeFailureThreshold = 3 private const val useOnionRequests = true - const val useTestnet = true + const val useTestnet = false // Error internal sealed class Error(val description: String) : Exception(description) { From 8c4bd9b44883fbc4ebfda8d707ddd8223a7e838f Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Tue, 9 Jul 2024 11:45:30 +1000 Subject: [PATCH 07/10] Release/1.18.5 (#1536) * Fix issue with span being the full length (#1528) * Proper display of unresolved names in mentions (#1530) * Fix issue with span being the full length * Making sure a mention with a username without a resolved name still displayed with the appropriate style with the truncated is * Testnet build (#1532) Co-authored-by: fanchao * Allow "public.loki.foundation" to be accessed by http (#1534) Co-authored-by: fanchao * Bumping the version code and name * Reverting temporary change --------- Co-authored-by: Fanchao Liu <273191+simophin@users.noreply.github.com> Co-authored-by: fanchao --- app/build.gradle | 4 ++-- .../conversation/v2/utilities/MentionUtilities.kt | 3 ++- .../securesms/util/RoundedBackgroundSpan.kt | 12 +++++++++++- .../main/res/xml/network_security_configuration.xml | 1 + .../java/org/session/libsession/utilities/IdUtil.kt | 2 +- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index aa3ad4e779..2e52adddd5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,8 +31,8 @@ configurations.all { exclude module: "commons-logging" } -def canonicalVersionCode = 373 -def canonicalVersionName = "1.18.4" +def canonicalVersionCode = 374 +def canonicalVersionName = "1.18.5" def postFixSize = 10 def abiPostFix = ['armeabi-v7a' : 1, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt index 3edfcffc97..3a4bfd6816 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt @@ -18,6 +18,7 @@ import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.getColorFromAttr +import org.session.libsession.utilities.truncateIdForDisplay import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.util.RoundedBackgroundSpan import org.thoughtcrime.securesms.util.getAccentColor @@ -67,7 +68,7 @@ object MentionUtilities { } else { val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey) @Suppress("NAME_SHADOWING") val context = if (openGroup != null) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR - contact?.displayName(context) + contact?.displayName(context) ?: truncateIdForDisplay(publicKey) } if (userDisplayName != null) { val mention = "@$userDisplayName" diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/RoundedBackgroundSpan.kt b/app/src/main/java/org/thoughtcrime/securesms/util/RoundedBackgroundSpan.kt index ebefc9c50c..a4cf1b1e96 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/RoundedBackgroundSpan.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/RoundedBackgroundSpan.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.util import android.content.Context -import android.content.res.Resources import android.graphics.Canvas import android.graphics.Paint import android.graphics.RectF @@ -50,6 +49,17 @@ class RoundedBackgroundSpan( override fun getSize( paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt? ): Int { + // If the span covers the whole text, and the height is not set, draw() will not be called for the span. + // To help with that we need to take the font metric into account + val metrics = paint.fontMetricsInt + if (fm != null) { + fm.top = metrics.top + fm.ascent = metrics.ascent + fm.descent = metrics.descent + + fm.bottom = metrics.bottom + } + return (paint.measureText(text, start, end) + 2 * paddingHorizontal).toInt() } diff --git a/app/src/main/res/xml/network_security_configuration.xml b/app/src/main/res/xml/network_security_configuration.xml index f3a7419b55..7469ebe106 100644 --- a/app/src/main/res/xml/network_security_configuration.xml +++ b/app/src/main/res/xml/network_security_configuration.xml @@ -2,6 +2,7 @@ 127.0.0.1 + public.loki.foundation seed1.getsession.org diff --git a/libsession/src/main/java/org/session/libsession/utilities/IdUtil.kt b/libsession/src/main/java/org/session/libsession/utilities/IdUtil.kt index ccaa31c2f9..1902b6e0ab 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/IdUtil.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/IdUtil.kt @@ -1,4 +1,4 @@ package org.session.libsession.utilities fun truncateIdForDisplay(id: String): String = - id.takeIf { it.length > 8 }?.apply{ "${take(4)}…${takeLast(4)}" } ?: id + id.takeIf { it.length > 8 }?.run{ "${take(4)}…${takeLast(4)}" } ?: id From ad0b1a9db72c03b68dcf97ec3dc66dbb985273a9 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Tue, 9 Jul 2024 14:07:25 +1000 Subject: [PATCH 08/10] Revert "Release/1.18.5 (#1536)" This reverts commit 8c4bd9b44883fbc4ebfda8d707ddd8223a7e838f. --- app/build.gradle | 4 ++-- .../conversation/v2/utilities/MentionUtilities.kt | 3 +-- .../securesms/util/RoundedBackgroundSpan.kt | 12 +----------- .../main/res/xml/network_security_configuration.xml | 1 - .../java/org/session/libsession/utilities/IdUtil.kt | 2 +- 5 files changed, 5 insertions(+), 17 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2e52adddd5..aa3ad4e779 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,8 +31,8 @@ configurations.all { exclude module: "commons-logging" } -def canonicalVersionCode = 374 -def canonicalVersionName = "1.18.5" +def canonicalVersionCode = 373 +def canonicalVersionName = "1.18.4" def postFixSize = 10 def abiPostFix = ['armeabi-v7a' : 1, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt index 3a4bfd6816..3edfcffc97 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt @@ -18,7 +18,6 @@ import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.getColorFromAttr -import org.session.libsession.utilities.truncateIdForDisplay import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.util.RoundedBackgroundSpan import org.thoughtcrime.securesms.util.getAccentColor @@ -68,7 +67,7 @@ object MentionUtilities { } else { val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey) @Suppress("NAME_SHADOWING") val context = if (openGroup != null) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR - contact?.displayName(context) ?: truncateIdForDisplay(publicKey) + contact?.displayName(context) } if (userDisplayName != null) { val mention = "@$userDisplayName" diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/RoundedBackgroundSpan.kt b/app/src/main/java/org/thoughtcrime/securesms/util/RoundedBackgroundSpan.kt index a4cf1b1e96..ebefc9c50c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/RoundedBackgroundSpan.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/RoundedBackgroundSpan.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.util import android.content.Context +import android.content.res.Resources import android.graphics.Canvas import android.graphics.Paint import android.graphics.RectF @@ -49,17 +50,6 @@ class RoundedBackgroundSpan( override fun getSize( paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt? ): Int { - // If the span covers the whole text, and the height is not set, draw() will not be called for the span. - // To help with that we need to take the font metric into account - val metrics = paint.fontMetricsInt - if (fm != null) { - fm.top = metrics.top - fm.ascent = metrics.ascent - fm.descent = metrics.descent - - fm.bottom = metrics.bottom - } - return (paint.measureText(text, start, end) + 2 * paddingHorizontal).toInt() } diff --git a/app/src/main/res/xml/network_security_configuration.xml b/app/src/main/res/xml/network_security_configuration.xml index 7469ebe106..f3a7419b55 100644 --- a/app/src/main/res/xml/network_security_configuration.xml +++ b/app/src/main/res/xml/network_security_configuration.xml @@ -2,7 +2,6 @@ 127.0.0.1 - public.loki.foundation seed1.getsession.org diff --git a/libsession/src/main/java/org/session/libsession/utilities/IdUtil.kt b/libsession/src/main/java/org/session/libsession/utilities/IdUtil.kt index 1902b6e0ab..ccaa31c2f9 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/IdUtil.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/IdUtil.kt @@ -1,4 +1,4 @@ package org.session.libsession.utilities fun truncateIdForDisplay(id: String): String = - id.takeIf { it.length > 8 }?.run{ "${take(4)}…${takeLast(4)}" } ?: id + id.takeIf { it.length > 8 }?.apply{ "${take(4)}…${takeLast(4)}" } ?: id From 2e3acd902ff53ff703cfa314413062de7c553869 Mon Sep 17 00:00:00 2001 From: AL-Session <160798022+AL-Session@users.noreply.github.com> Date: Tue, 9 Jul 2024 16:23:26 +1000 Subject: [PATCH 09/10] SS-2168 - User profile warnings (#1531) * Profile picture upload fail informs user * End of day push * Push before trying with okhttp library update * WIP * Further WIP * Add additional debug comments * Push before cleanup * Cleaned up * More cleanup * Minor adjustment * Final cleanup prior to PR review * Removed commented out old conscrypt version import * Addressed PR feeback from Fanchao --------- Co-authored-by: alansley --- app/build.gradle | 2 +- .../v2/messages/VisibleMessageContentView.kt | 3 +- .../securesms/database/RecipientDatabase.java | 10 +-- .../securesms/database/Storage.kt | 20 +++-- .../securesms/groups/OpenGroupManager.kt | 5 +- .../securesms/logging/AndroidLogger.java | 3 +- .../securesms/notifications/PushRegistryV2.kt | 3 +- .../securesms/preferences/SettingsActivity.kt | 90 +++++++++++++------ .../util/ConfigurationMessageUtilities.kt | 14 +-- gradle.properties | 4 +- .../messaging/file_server/FileServerApi.kt | 17 ++-- .../messaging/jobs/AttachmentDownloadJob.kt | 5 +- .../messaging/jobs/AttachmentUploadJob.kt | 2 +- .../messaging/jobs/BackgroundGroupAddJob.kt | 5 +- .../messaging/jobs/ConfigurationSyncJob.kt | 3 +- .../session/libsession/messaging/jobs/Job.kt | 2 +- .../messaging/jobs/MessageSendJob.kt | 6 +- .../messaging/jobs/NotifyPNServerJob.kt | 8 +- .../messaging/open_groups/OpenGroup.kt | 9 +- .../messaging/open_groups/OpenGroupApi.kt | 11 ++- .../notifications/PushRegistryV1.kt | 7 +- .../libsession/snode/OnionRequestAPI.kt | 14 +-- .../snode/utilities/OKHTTPUtilities.kt | 8 +- .../libsession/utilities/DownloadUtilities.kt | 5 +- .../utilities/OpenGroupUrlParser.kt | 9 +- .../libsession/utilities/WindowDebouncer.kt | 4 +- libsession/src/main/res/values/strings.xml | 3 + .../org/session/libsignal/utilities/HTTP.kt | 14 +-- 28 files changed, 173 insertions(+), 113 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2e52adddd5..23bbecd217 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -271,7 +271,7 @@ dependencies { if (project.hasProperty('huawei')) huaweiImplementation 'com.huawei.hms:push:6.7.0.300' implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1' implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1' - implementation 'org.conscrypt:conscrypt-android:2.0.0' + implementation 'org.conscrypt:conscrypt-android:2.5.2' implementation 'org.signal:aesgcmprovider:0.0.3' implementation 'org.webrtc:google-webrtc:1.0.32006' implementation "me.leolin:ShortcutBadger:1.1.16" 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 83c6904dec..b320e72e26 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 @@ -22,6 +22,7 @@ import androidx.core.view.isVisible import network.loki.messenger.R import network.loki.messenger.databinding.ViewVisibleMessageContentBinding import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment @@ -289,7 +290,7 @@ class VisibleMessageContentView : ConstraintLayout { // replace URLSpans with ModalURLSpans body.getSpans(0, body.length).toList().forEach { urlSpan -> - val updatedUrl = urlSpan.url.let { HttpUrl.parse(it).toString() } + val updatedUrl = urlSpan.url.let { it.toHttpUrlOrNull().toString() } val replacementSpan = ModalURLSpan(updatedUrl) { url -> val activity = context as AppCompatActivity ModalUrlBottomSheet(url).show(activity.supportFragmentManager, "Open URL Dialog") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index 8dbef32017..dcd7778c9a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -55,7 +55,7 @@ public class RecipientDatabase extends Database { private static final String SYSTEM_PHONE_LABEL = "system_phone_label"; private static final String SYSTEM_CONTACT_URI = "system_contact_uri"; private static final String SIGNAL_PROFILE_NAME = "signal_profile_name"; - private static final String SIGNAL_PROFILE_AVATAR = "signal_profile_avatar"; + private static final String SESSION_PROFILE_AVATAR = "signal_profile_avatar"; private static final String PROFILE_SHARING = "profile_sharing_approval"; private static final String CALL_RINGTONE = "call_ringtone"; private static final String CALL_VIBRATE = "call_vibrate"; @@ -69,7 +69,7 @@ public class RecipientDatabase extends Database { private static final String[] RECIPIENT_PROJECTION = new String[] { BLOCK, APPROVED, APPROVED_ME, NOTIFICATION, CALL_RINGTONE, VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, EXPIRE_MESSAGES, REGISTERED, PROFILE_KEY, SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_CONTACT_URI, - SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL, + SIGNAL_PROFILE_NAME, SESSION_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL, UNIDENTIFIED_ACCESS_MODE, FORCE_SMS_SELECTION, NOTIFY_TYPE, DISAPPEARING_STATE, WRAPPER_HASH, BLOCKS_COMMUNITY_MESSAGE_REQUESTS }; @@ -97,7 +97,7 @@ public class RecipientDatabase extends Database { SYSTEM_CONTACT_URI + " TEXT DEFAULT NULL, " + PROFILE_KEY + " TEXT DEFAULT NULL, " + SIGNAL_PROFILE_NAME + " TEXT DEFAULT NULL, " + - SIGNAL_PROFILE_AVATAR + " TEXT DEFAULT NULL, " + + SESSION_PROFILE_AVATAR + " TEXT DEFAULT NULL, " + PROFILE_SHARING + " INTEGER DEFAULT 0, " + CALL_RINGTONE + " TEXT DEFAULT NULL, " + CALL_VIBRATE + " INTEGER DEFAULT " + Recipient.VibrateState.DEFAULT.getId() + ", " + @@ -204,7 +204,7 @@ public class RecipientDatabase extends Database { String systemPhoneLabel = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_PHONE_LABEL)); String systemContactUri = cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_CONTACT_URI)); String signalProfileName = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_NAME)); - String signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SIGNAL_PROFILE_AVATAR)); + String signalProfileAvatar = cursor.getString(cursor.getColumnIndexOrThrow(SESSION_PROFILE_AVATAR)); boolean profileSharing = cursor.getInt(cursor.getColumnIndexOrThrow(PROFILE_SHARING)) == 1; String notificationChannel = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION_CHANNEL)); int unidentifiedAccessMode = cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED_ACCESS_MODE)); @@ -361,7 +361,7 @@ public class RecipientDatabase extends Database { public void setProfileAvatar(@NonNull Recipient recipient, @Nullable String profileAvatar) { ContentValues contentValues = new ContentValues(1); - contentValues.put(SIGNAL_PROFILE_AVATAR, profileAvatar); + contentValues.put(SESSION_PROFILE_AVATAR, profileAvatar); updateOrInsert(recipient.getAddress(), contentValues); recipient.resolve().setProfileAvatar(profileAvatar); notifyRecipientListeners(); 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 dd0544b420..29c637bc23 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database import android.content.Context import android.net.Uri +import java.security.MessageDigest import network.loki.messenger.libsession_util.ConfigBase import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_HIDDEN import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_PINNED @@ -10,6 +11,7 @@ import network.loki.messenger.libsession_util.ConversationVolatileConfig import network.loki.messenger.libsession_util.UserGroupsConfig import network.loki.messenger.libsession_util.UserProfile import network.loki.messenger.libsession_util.util.BaseCommunityInfo +import network.loki.messenger.libsession_util.util.Contact as LibSessionContact import network.loki.messenger.libsession_util.util.Conversation import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.GroupInfo @@ -91,8 +93,6 @@ import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.mms.PartAuthority import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.SessionMetaProtocol -import java.security.MessageDigest -import network.loki.messenger.libsession_util.util.Contact as LibSessionContact private const val TAG = "Storage" @@ -471,7 +471,8 @@ open class Storage( val userPublicKey = getUserPublicKey() ?: return // would love to get rid of recipient and context from this val recipient = Recipient.from(context, fromSerialized(userPublicKey), false) - // update name + + // Update profile name val name = userProfile.getName() ?: return val userPic = userProfile.getPic() val profileManager = SSKEnvironment.shared.profileManager @@ -480,13 +481,14 @@ open class Storage( profileManager.setName(context, recipient, name) } - // update pfp + // Update profile picture if (userPic == UserPic.DEFAULT) { clearUserPic() } else if (userPic.key.isNotEmpty() && userPic.url.isNotEmpty() && TextSecurePreferences.getProfilePictureURL(context) != userPic.url) { setUserProfilePicture(userPic.url, userPic.key) } + if (userProfile.getNtsPriority() == PRIORITY_HIDDEN) { // delete nts thread if needed val ourThread = getThreadId(recipient) ?: return @@ -514,12 +516,13 @@ open class Storage( addLibSessionContacts(extracted, messageTimestamp) } - override fun clearUserPic() { - val userPublicKey = getUserPublicKey() ?: return + override fun clearUserPic() { + val userPublicKey = getUserPublicKey() ?: return Log.w(TAG, "No user public key when trying to clear user pic") val recipientDatabase = DatabaseComponent.get(context).recipientDatabase() - // would love to get rid of recipient and context from this + val recipient = Recipient.from(context, fromSerialized(userPublicKey), false) - // clear picture if userPic is null + + // Clear details related to the user's profile picture TextSecurePreferences.setProfileKey(context, null) ProfileKeyUtil.setEncodedProfileKey(context, null) recipientDatabase.setProfileAvatar(recipient, null) @@ -528,7 +531,6 @@ open class Storage( Recipient.removeCached(fromSerialized(userPublicKey)) configFactory.user?.setPic(UserPic.DEFAULT) - ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) } private fun updateConvoVolatile(convos: ConversationVolatileConfig, messageTimestamp: Long) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt index 01e1c514ff..8bb7a39d4a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.groups import android.content.Context import androidx.annotation.WorkerThread import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.open_groups.GroupMemberRole import org.session.libsession.messaging.open_groups.OpenGroup @@ -143,9 +144,9 @@ object OpenGroupManager { @WorkerThread fun addOpenGroup(urlAsString: String, context: Context): OpenGroupApi.RoomInfo? { - val url = HttpUrl.parse(urlAsString) ?: return null + val url = urlAsString.toHttpUrlOrNull() ?: return null val server = OpenGroup.getServer(urlAsString) - val room = url.pathSegments().firstOrNull() ?: return null + val room = url.pathSegments.firstOrNull() ?: return null val publicKey = url.queryParameter("public_key") ?: return null return add(server.toString().removeSuffix("/"), room, publicKey, context).second // assume migrated from calling function diff --git a/app/src/main/java/org/thoughtcrime/securesms/logging/AndroidLogger.java b/app/src/main/java/org/thoughtcrime/securesms/logging/AndroidLogger.java index d62aac647c..196e77e45a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logging/AndroidLogger.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logging/AndroidLogger.java @@ -35,6 +35,5 @@ public class AndroidLogger extends Log.Logger { } @Override - public void blockUntilAllWritesFinished() { - } + public void blockUntilAllWritesFinished() { } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistryV2.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistryV2.kt index bf16333b15..42ae798366 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistryV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/PushRegistryV2.kt @@ -10,6 +10,7 @@ import kotlinx.serialization.json.decodeFromStream import nl.komponents.kovenant.Promise import nl.komponents.kovenant.functional.map import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaType import okhttp3.Request import okhttp3.RequestBody import org.session.libsession.messaging.sending_receiving.notifications.Response @@ -99,7 +100,7 @@ class PushRegistryV2 @Inject constructor(private val pushReceiver: PushReceiver) private inline fun getResponseBody(path: String, requestParameters: String): Promise { val server = Server.LATEST val url = "${server.url}/$path" - val body = RequestBody.create(MediaType.get("application/json"), requestParameters) + val body = RequestBody.create("application/json".toMediaType(), requestParameters) val request = Request.Builder().url(url).post(body).build() return OnionRequestAPI.sendOnionRequest( 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 b66df5d255..e423b44170 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -22,13 +22,16 @@ import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.core.view.isVisible import dagger.hilt.android.AndroidEntryPoint +import java.io.File +import java.security.SecureRandom +import javax.inject.Inject import network.loki.messenger.BuildConfig import network.loki.messenger.R import network.loki.messenger.databinding.ActivitySettingsBinding import network.loki.messenger.libsession_util.util.UserPic import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.all import nl.komponents.kovenant.ui.alwaysUi +import nl.komponents.kovenant.ui.failUi import nl.komponents.kovenant.ui.successUi import org.session.libsession.avatars.AvatarHelper import org.session.libsession.avatars.ProfileContactPhoto @@ -37,7 +40,7 @@ import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.* import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol import org.session.libsession.utilities.recipients.Recipient -import org.session.libsignal.utilities.getProperty +import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.avatar.AvatarSelection import org.thoughtcrime.securesms.components.ProfilePictureView @@ -56,12 +59,10 @@ import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.disableClipping import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.show -import java.io.File -import java.security.SecureRandom -import javax.inject.Inject @AndroidEntryPoint class SettingsActivity : PassphraseRequiredActionBarActivity() { + private val TAG = "SettingsActivity" @Inject lateinit var configFactory: ConfigFactory @@ -233,41 +234,78 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { displayName: String? = null ) { binding.loader.isVisible = true - val promises = mutableListOf>() + if (displayName != null) { TextSecurePreferences.setProfileName(this, displayName) configFactory.user?.setName(displayName) } + + // Bail if we're not updating the profile picture in any way + if (!isUpdatingProfilePicture) return + val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this) - if (isUpdatingProfilePicture) { - if (profilePicture != null) { - promises.add(ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this)) - } else { + + val uploadProfilePicturePromise: Promise<*, Exception> + var removingProfilePic = false + + // Adding a new profile picture? + if (profilePicture != null) { + uploadProfilePicturePromise = ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this) + } else { + // If not then we must be removing the existing one. + // Note: To get a promise that will resolve / sync correctly we overwrite the existing profile picture with + // a 0 byte image. + removingProfilePic = true + val emptyByteArray = ByteArray(0) + uploadProfilePicturePromise = ProfilePictureUtilities.upload(emptyByteArray, encodedProfileKey, this) + } + + // If the upload picture promise succeeded then we hit this successUi block + uploadProfilePicturePromise.successUi { + + // If we successfully removed the profile picture on the network then we can clear the + // local data - otherwise it's weird to fail the online section but it _looks_ like it + // worked because we cleared the local image (also it denies them the chance to retry + // removal if we do it locally, and may result in them having a visible profile picture + // everywhere EXCEPT on their own device!). + if (removingProfilePic) { MessagingModuleConfiguration.shared.storage.clearUserPic() } - } - val compoundPromise = all(promises) - compoundPromise.successUi { // Do this on the UI thread so that it happens before the alwaysUi clause below + val userConfig = configFactory.user - if (isUpdatingProfilePicture) { - AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture) - TextSecurePreferences.setProfileAvatarId(this, profilePicture?.let { SecureRandom().nextInt() } ?: 0 ) - ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey) - // new config - val url = TextSecurePreferences.getProfilePictureURL(this) - val profileKey = ProfileKeyUtil.getProfileKey(this) - if (profilePicture == null) { - userConfig?.setPic(UserPic.DEFAULT) - } else if (!url.isNullOrEmpty() && profileKey.isNotEmpty()) { - userConfig?.setPic(UserPic(url, profileKey)) - } + AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture) + TextSecurePreferences.setProfileAvatarId(this, profilePicture?.let { SecureRandom().nextInt() } ?: 0 ) + ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey) + + // new config + val url = TextSecurePreferences.getProfilePictureURL(this) + val profileKey = ProfileKeyUtil.getProfileKey(this) + + // If we have a URL and a profile key then set the user's profile picture + if (!url.isNullOrEmpty() && profileKey.isNotEmpty()) { + userConfig?.setPic(UserPic(url, profileKey)) } + if (userConfig != null && userConfig.needsDump()) { configFactory.persist(userConfig, SnodeAPI.nowWithOffset) } + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@SettingsActivity) } - compoundPromise.alwaysUi { + + // Or if the promise failed to upload the new profile picture then we hit this failUi block + uploadProfilePicturePromise.failUi { + if (removingProfilePic) { + Log.e(TAG, "Failed to remove profile picture") + Toast.makeText(this@SettingsActivity, R.string.profileDisplayPictureRemoveError, Toast.LENGTH_LONG).show() + } else { + Log.e(TAG, "Failed to upload profile picture") + Toast.makeText(this@SettingsActivity, R.string.profileErrorUpdate, Toast.LENGTH_LONG).show() + } + } + + // Finally, regardless of whether the promise succeeded or failed, we always hit this `alwaysUi` block + uploadProfilePicturePromise.alwaysUi { if (displayName != null) { binding.btnGroupNameDisplay.text = displayName } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt index e0f0492445..a467614376 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt @@ -24,6 +24,7 @@ import org.session.libsession.utilities.WindowDebouncer import org.session.libsignal.crypto.ecc.DjbECPublicKey import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.IdPrefix +import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.toHexString import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.ThreadDatabase @@ -31,10 +32,12 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent import java.util.Timer object ConfigurationMessageUtilities { + private const val TAG = "ConfigMessageUtils" private val debouncer = WindowDebouncer(3000, Timer()) private fun scheduleConfigSync(userPublicKey: String) { + debouncer.publish { // don't schedule job if we already have one val storage = MessagingModuleConfiguration.shared.storage @@ -44,23 +47,20 @@ object ConfigurationMessageUtilities { (currentStorageJob as ConfigurationSyncJob).shouldRunAgain.set(true) return@publish } - val newConfigSync = ConfigurationSyncJob(ourDestination) - JobQueue.shared.add(newConfigSync) + val newConfigSyncJob = ConfigurationSyncJob(ourDestination) + JobQueue.shared.add(newConfigSyncJob) } } @JvmStatic fun syncConfigurationIfNeeded(context: Context) { - // add if check here to schedule new config job process and return early - val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return + val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Log.w(TAG, "User Public Key is null") scheduleConfigSync(userPublicKey) } fun forceSyncConfigurationNowIfNeeded(context: Context): Promise { - // add if check here to schedule new config job process and return early val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return Promise.ofFail(NullPointerException("User Public Key is null")) - // schedule job if none exist - // don't schedule job if we already have one + // Schedule a new job if one doesn't already exist (only) scheduleConfigSync(userPublicKey) return Promise.ofSuccess(Unit) } diff --git a/gradle.properties b/gradle.properties index 1d7bc62d40..f41dc761bc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,7 @@ googleServicesVersion=4.3.12 kotlinVersion=1.8.21 android.useAndroidX=true appcompatVersion=1.6.1 -coreVersion=1.8.0 +coreVersion=1.13.1 coroutinesVersion=1.6.4 curve25519Version=0.6.0 daggerVersion=2.46.1 @@ -33,7 +33,7 @@ kovenantVersion=3.3.0 lifecycleVersion=2.5.1 materialVersion=1.8.0 mockitoKotlinVersion=4.1.0 -okhttpVersion=3.12.1 +okhttpVersion=4.12.0 pagingVersion=3.0.0 preferenceVersion=1.2.0 protobufVersion=2.5.0 diff --git a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt index 0e8768d530..5bffed57ee 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt @@ -3,8 +3,11 @@ package org.session.libsession.messaging.file_server import nl.komponents.kovenant.Promise import nl.komponents.kovenant.functional.map import okhttp3.Headers +import okhttp3.Headers.Companion.toHeaders import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody import org.session.libsession.snode.OnionRequestAPI import org.session.libsignal.utilities.HTTP @@ -37,18 +40,18 @@ object FileServerApi { ) private fun createBody(body: ByteArray?, parameters: Any?): RequestBody? { - if (body != null) return RequestBody.create(MediaType.get("application/octet-stream"), body) + if (body != null) return RequestBody.create("application/octet-stream".toMediaType(), body) if (parameters == null) return null val parametersAsJSON = JsonUtil.toJson(parameters) - return RequestBody.create(MediaType.get("application/json"), parametersAsJSON) + return RequestBody.create("application/json".toMediaType(), parametersAsJSON) } private fun send(request: Request): Promise { - val url = HttpUrl.parse(server) ?: return Promise.ofFail(Error.InvalidURL) + val url = server.toHttpUrlOrNull() ?: return Promise.ofFail(Error.InvalidURL) val urlBuilder = HttpUrl.Builder() - .scheme(url.scheme()) - .host(url.host()) - .port(url.port()) + .scheme(url.scheme) + .host(url.host) + .port(url.port) .addPathSegments(request.endpoint) if (request.verb == HTTP.Verb.GET) { for ((key, value) in request.queryParameters) { @@ -57,7 +60,7 @@ object FileServerApi { } val requestBuilder = okhttp3.Request.Builder() .url(urlBuilder.build()) - .headers(Headers.of(request.headers)) + .headers(request.headers.toHeaders()) when (request.verb) { HTTP.Verb.GET -> requestBuilder.get() HTTP.Verb.PUT -> requestBuilder.put(createBody(request.body, request.parameters)!!) 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 ffa05bf1e6..0da398dff2 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 @@ -1,6 +1,7 @@ package org.session.libsession.messaging.jobs import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.session.libsession.database.MessageDataProvider import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.MessagingModuleConfiguration @@ -141,8 +142,8 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) DownloadUtilities.downloadFile(tempFile, attachment.url) } else { Log.d("AttachmentDownloadJob", "downloading open group attachment") - val url = HttpUrl.parse(attachment.url)!! - val fileID = url.pathSegments().last() + val url = attachment.url.toHttpUrlOrNull()!! + val fileID = url.pathSegments.last() OpenGroupApi.download(fileID, openGroup.room, openGroup.server).get().let { tempFile.writeBytes(it) } 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 19b6555b50..1e6d483603 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 @@ -176,7 +176,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess val kryo = Kryo() kryo.isRegistrationRequired = false val serializedMessage = ByteArray(4096) - val output = Output(serializedMessage, Job.MAX_BUFFER_SIZE) + val output = Output(serializedMessage, Job.MAX_BUFFER_SIZE_BYTES) kryo.writeClassAndObject(output, message) output.close() return Data.Builder() diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt index f284f2539d..5f7bb34ce4 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt @@ -1,6 +1,7 @@ package org.session.libsession.messaging.jobs import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.utilities.Data @@ -21,9 +22,9 @@ class BackgroundGroupAddJob(val joinUrl: String): Job { override val maxFailureCount: Int = 1 val openGroupId: String? get() { - val url = HttpUrl.parse(joinUrl) ?: return null + val url = joinUrl.toHttpUrlOrNull() ?: return null val server = OpenGroup.getServer(joinUrl)?.toString()?.removeSuffix("/") ?: return null - val room = url.pathSegments().firstOrNull() ?: return null + val room = url.pathSegments.firstOrNull() ?: return null return "$server.$room" } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt index d7dfdf768e..4a3299d197 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt @@ -1,5 +1,6 @@ package org.session.libsession.messaging.jobs +import java.util.concurrent.atomic.AtomicBoolean import network.loki.messenger.libsession_util.ConfigBase.Companion.protoKindFor import nl.komponents.kovenant.functional.bind import org.session.libsession.messaging.MessagingModuleConfiguration @@ -10,7 +11,6 @@ import org.session.libsession.messaging.utilities.Data import org.session.libsession.snode.RawResponse import org.session.libsession.snode.SnodeAPI import org.session.libsignal.utilities.Log -import java.util.concurrent.atomic.AtomicBoolean // only contact (self) and closed group destinations will be supported data class ConfigurationSyncJob(val destination: Destination): Job { @@ -180,7 +180,6 @@ data class ConfigurationSyncJob(val destination: Destination): Job { // type mappings const val CONTACT_TYPE = 1 const val GROUP_TYPE = 2 - } class Factory: Job.Factory { diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/Job.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/Job.kt index 7f3bf9b173..8e9bcf839c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/Job.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/Job.kt @@ -14,7 +14,7 @@ interface Job { // Keys used for database storage private val ID_KEY = "id" private val FAILURE_COUNT_KEY = "failure_count" - internal const val MAX_BUFFER_SIZE = 1_000_000 // bytes + internal const val MAX_BUFFER_SIZE_BYTES = 1_000_000 // ~1MB } suspend fun execute(dispatcherName: String) diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt index 2a152d0a01..52d56184cc 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt @@ -4,7 +4,7 @@ import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import org.session.libsession.messaging.MessagingModuleConfiguration -import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE +import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE_BYTES import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.visible.VisibleMessage @@ -118,12 +118,12 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job { val kryo = Kryo() kryo.isRegistrationRequired = false // Message - val messageOutput = Output(ByteArray(4096), MAX_BUFFER_SIZE) + val messageOutput = Output(ByteArray(4096), MAX_BUFFER_SIZE_BYTES) kryo.writeClassAndObject(messageOutput, message) messageOutput.close() val serializedMessage = messageOutput.toBytes() // Destination - val destinationOutput = Output(ByteArray(4096), MAX_BUFFER_SIZE) + val destinationOutput = Output(ByteArray(4096), MAX_BUFFER_SIZE_BYTES) kryo.writeClassAndObject(destinationOutput, destination) destinationOutput.close() val serializedDestination = destinationOutput.toBytes() diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt index 79c30f67e8..26a0cfb6e8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt @@ -3,10 +3,10 @@ package org.session.libsession.messaging.jobs import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output -import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaType import okhttp3.Request import okhttp3.RequestBody -import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE +import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE_BYTES import org.session.libsession.messaging.sending_receiving.notifications.Server import org.session.libsession.messaging.utilities.Data import org.session.libsession.snode.OnionRequestAPI @@ -33,7 +33,7 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job { val server = Server.LEGACY val parameters = mapOf( "data" to message.data, "send_to" to message.recipient ) val url = "${server.url}/notify" - val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) + val body = RequestBody.create("application/json".toMediaType(), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body).build() retryIfNeeded(4) { OnionRequestAPI.sendOnionRequest( @@ -67,7 +67,7 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job { val kryo = Kryo() kryo.isRegistrationRequired = false val serializedMessage = ByteArray(4096) - val output = Output(serializedMessage, MAX_BUFFER_SIZE) + val output = Output(serializedMessage, MAX_BUFFER_SIZE_BYTES) kryo.writeObject(output, message) output.close() return Data.Builder() diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroup.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroup.kt index 80a9a1e501..7743cd8176 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroup.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroup.kt @@ -1,6 +1,7 @@ package org.session.libsession.messaging.open_groups import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.Log import java.util.Locale @@ -47,11 +48,11 @@ data class OpenGroup( } fun getServer(urlAsString: String): HttpUrl? { - val url = HttpUrl.parse(urlAsString) ?: return null - val builder = HttpUrl.Builder().scheme(url.scheme()).host(url.host()) - if (url.port() != 80 || url.port() != 443) { + val url = urlAsString.toHttpUrlOrNull() ?: return null + val builder = HttpUrl.Builder().scheme(url.scheme).host(url.host) + if (url.port != 80 || url.port != 443) { // Non-standard port; add to server - builder.port(url.port()) + builder.port(url.port) } return builder.build() } diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt index 6e73c16f5e..50eb7be7b0 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt @@ -14,8 +14,11 @@ import kotlinx.coroutines.flow.MutableSharedFlow import nl.komponents.kovenant.Promise import nl.komponents.kovenant.functional.map import okhttp3.Headers +import okhttp3.Headers.Companion.toHeaders import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller.Companion.maxInactivityPeriod @@ -282,10 +285,10 @@ object OpenGroupApi { ) private fun createBody(body: ByteArray?, parameters: Any?): RequestBody? { - if (body != null) return RequestBody.create(MediaType.get("application/octet-stream"), body) + if (body != null) return RequestBody.create("application/octet-stream".toMediaType(), body) if (parameters == null) return null val parametersAsJSON = JsonUtil.toJson(parameters) - return RequestBody.create(MediaType.get("application/json"), parametersAsJSON) + return RequestBody.create("application/json".toMediaType(), parametersAsJSON) } private fun getResponseBody(request: Request): Promise { @@ -301,7 +304,7 @@ object OpenGroupApi { } private fun send(request: Request): Promise { - HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.InvalidURL) + request.server.toHttpUrlOrNull() ?: return Promise.ofFail(Error.InvalidURL) val urlBuilder = StringBuilder("${request.server}/${request.endpoint.value}") if (request.verb == GET && request.queryParameters.isNotEmpty()) { urlBuilder.append("?") @@ -387,7 +390,7 @@ object OpenGroupApi { val requestBuilder = okhttp3.Request.Builder() .url(urlRequest) - .headers(Headers.of(headers)) + .headers(headers.toHeaders()) when (request.verb) { GET -> requestBuilder.get() PUT -> requestBuilder.put(createBody(request.body, request.parameters)!!) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushRegistryV1.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushRegistryV1.kt index 1599dd93d5..230fb2dc9c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushRegistryV1.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushRegistryV1.kt @@ -3,6 +3,7 @@ package org.session.libsession.messaging.sending_receiving.notifications import android.annotation.SuppressLint import nl.komponents.kovenant.Promise import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaType import okhttp3.Request import okhttp3.RequestBody import org.session.libsession.messaging.MessagingModuleConfiguration @@ -58,7 +59,7 @@ object PushRegistryV1 { val url = "${server.url}/register_legacy_groups_only" val body = RequestBody.create( - MediaType.get("application/json"), + "application/json".toMediaType(), JsonUtil.toJson(parameters) ) val request = Request.Builder().url(url).post(body).build() @@ -83,7 +84,7 @@ object PushRegistryV1 { return retryIfNeeded(maxRetryCount) { val parameters = mapOf("token" to token) val url = "${server.url}/unregister" - val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) + val body = RequestBody.create("application/json".toMediaType(), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body).build() sendOnionRequest(request) success { @@ -120,7 +121,7 @@ object PushRegistryV1 { ): Promise<*, Exception> { val parameters = mapOf("closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey) val url = "${server.url}/$operation" - val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) + val body = RequestBody.create("application/json".toMediaType(), JsonUtil.toJson(parameters)) val request = Request.Builder().url(url).post(body).build() return retryIfNeeded(maxRetryCount) { diff --git a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt index cf43c7b14a..8e55f286bc 100644 --- a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt @@ -467,9 +467,9 @@ object OnionRequestAPI { x25519PublicKey: String, version: Version = Version.V4 ): Promise { - val url = request.url() + val url = request.url val payload = generatePayload(request, server, version) - val destination = Destination.Server(url.host(), version.value, x25519PublicKey, url.scheme(), url.port()) + val destination = Destination.Server(url.host, version.value, x25519PublicKey, url.scheme, url.port) return sendOnionRequest(destination, payload, version).recover { exception -> Log.d("Loki", "Couldn't reach server: $url due to error: $exception.") throw exception @@ -478,7 +478,7 @@ object OnionRequestAPI { private fun generatePayload(request: Request, server: String, version: Version): ByteArray { val headers = request.getHeadersForOnionRequest().toMutableMap() - val url = request.url() + val url = request.url val urlAsString = url.toString() val body = request.getBodyForOnionRequest() ?: "null" val endpoint = when { @@ -486,19 +486,19 @@ object OnionRequestAPI { else -> "" } return if (version == Version.V4) { - if (request.body() != null && + if (request.body != null && headers.keys.find { it.equals("Content-Type", true) } == null) { headers["Content-Type"] = "application/json" } val requestPayload = mapOf( "endpoint" to endpoint, - "method" to request.method(), + "method" to request.method, "headers" to headers ) val requestData = JsonUtil.toJson(requestPayload).toByteArray() val prefixData = "l${requestData.size}:".toByteArray(Charsets.US_ASCII) val suffixData = "e".toByteArray(Charsets.US_ASCII) - if (request.body() != null) { + if (request.body != null) { val bodyData = if (body is ByteArray) body else body.toString().toByteArray() val bodyLengthData = "${bodyData.size}:".toByteArray(Charsets.US_ASCII) prefixData + requestData + bodyLengthData + bodyData + suffixData @@ -509,7 +509,7 @@ object OnionRequestAPI { val payload = mapOf( "body" to body, "endpoint" to endpoint.removePrefix("/"), - "method" to request.method(), + "method" to request.method, "headers" to headers ) JsonUtil.toJson(payload).toByteArray() diff --git a/libsession/src/main/java/org/session/libsession/snode/utilities/OKHTTPUtilities.kt b/libsession/src/main/java/org/session/libsession/snode/utilities/OKHTTPUtilities.kt index 4c08c4791e..981664f2d3 100644 --- a/libsession/src/main/java/org/session/libsession/snode/utilities/OKHTTPUtilities.kt +++ b/libsession/src/main/java/org/session/libsession/snode/utilities/OKHTTPUtilities.kt @@ -9,13 +9,13 @@ import java.util.Locale internal fun Request.getHeadersForOnionRequest(): Map { val result = mutableMapOf() - val contentType = body()?.contentType() + val contentType = body?.contentType() if (contentType != null) { result["content-type"] = contentType.toString() } - val headers = headers() + val headers = headers for (name in headers.names()) { - val value = headers.get(name) + val value = headers[name] if (value != null) { if (value.toLowerCase(Locale.US) == "true" || value.toLowerCase(Locale.US) == "false") { result[name] = value.toBoolean() @@ -33,7 +33,7 @@ internal fun Request.getBodyForOnionRequest(): Any? { try { val copyOfThis = newBuilder().build() val buffer = Buffer() - val body = copyOfThis.body() ?: return null + val body = copyOfThis.body ?: return null body.writeTo(buffer) val bodyAsData = buffer.readByteArray() if (body is MultipartBody) { 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 27b6b244ba..a7b19ed6e5 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt @@ -1,6 +1,7 @@ package org.session.libsession.utilities import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.session.libsession.messaging.file_server.FileServerApi import org.session.libsignal.utilities.HTTP import org.session.libsignal.utilities.Log @@ -36,8 +37,8 @@ object DownloadUtilities { */ @JvmStatic fun downloadFile(outputStream: OutputStream, urlAsString: String) { - val url = HttpUrl.parse(urlAsString)!! - val fileID = url.pathSegments().last() + val url = urlAsString.toHttpUrlOrNull()!! + val fileID = url.pathSegments.last() try { FileServerApi.download(fileID).get().let { outputStream.write(it) diff --git a/libsession/src/main/java/org/session/libsession/utilities/OpenGroupUrlParser.kt b/libsession/src/main/java/org/session/libsession/utilities/OpenGroupUrlParser.kt index d39128d5dc..cac7faf096 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/OpenGroupUrlParser.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/OpenGroupUrlParser.kt @@ -1,6 +1,7 @@ package org.session.libsession.utilities import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.session.libsession.messaging.open_groups.migrateLegacyServerUrl object OpenGroupUrlParser { @@ -19,14 +20,14 @@ object OpenGroupUrlParser { // URL has to start with 'http://' val urlWithPrefix = if (!string.startsWith("http")) "http://$string" else string // If the URL is malformed, throw an exception - val url = HttpUrl.parse(urlWithPrefix) ?: throw Error.MalformedURL + val url = urlWithPrefix.toHttpUrlOrNull() ?: throw Error.MalformedURL // Parse components - val server = HttpUrl.Builder().scheme(url.scheme()).host(url.host()).port(url.port()).build().toString().removeSuffix(suffix).migrateLegacyServerUrl() - val room = url.pathSegments().firstOrNull { !it.isNullOrEmpty() } ?: throw Error.NoRoom + val server = HttpUrl.Builder().scheme(url.scheme).host(url.host).port(url.port).build().toString().removeSuffix(suffix).migrateLegacyServerUrl() + val room = url.pathSegments.firstOrNull { !it.isNullOrEmpty() } ?: throw Error.NoRoom val publicKey = url.queryParameter(queryPrefix) ?: throw Error.NoPublicKey if (publicKey.length != 64) throw Error.InvalidPublicKey // Return - return V2OpenGroupInfo(server,room,publicKey) + return V2OpenGroupInfo(server, room, publicKey) } fun trimQueryParameter(string: String): String { diff --git a/libsession/src/main/java/org/session/libsession/utilities/WindowDebouncer.kt b/libsession/src/main/java/org/session/libsession/utilities/WindowDebouncer.kt index c3850e64e6..bec5c93837 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/WindowDebouncer.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/WindowDebouncer.kt @@ -9,7 +9,7 @@ import java.util.concurrent.atomic.AtomicReference * Not really a 'debouncer' but named to be similar to the current Debouncer * designed to queue tasks on a window (if not already queued) like a timer */ -class WindowDebouncer(private val window: Long, private val timer: Timer) { +class WindowDebouncer(private val timeWindowMilliseconds: Long, private val timer: Timer) { private val atomicRef: AtomicReference = AtomicReference(null) private val hasStarted = AtomicBoolean(false) @@ -23,7 +23,7 @@ class WindowDebouncer(private val window: Long, private val timer: Timer) { fun publish(runnable: Runnable) { if (hasStarted.compareAndSet(false, true)) { - timer.scheduleAtFixedRate(recursiveRunnable, 0, window) + timer.scheduleAtFixedRate(recursiveRunnable, 0, timeWindowMilliseconds) } atomicRef.compareAndSet(null, runnable) } diff --git a/libsession/src/main/res/values/strings.xml b/libsession/src/main/res/values/strings.xml index a6a9e9043b..53cc1bacca 100644 --- a/libsession/src/main/res/values/strings.xml +++ b/libsession/src/main/res/values/strings.xml @@ -80,4 +80,7 @@ Clear Device Clear device only Clear device and network + + Failed to remove display picture. + Failed to update profile. diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/HTTP.kt b/libsignal/src/main/java/org/session/libsignal/utilities/HTTP.kt index 5eac7cecd4..0c1b2f8317 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/HTTP.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/HTTP.kt @@ -1,6 +1,7 @@ package org.session.libsignal.utilities -import okhttp3.MediaType +import android.util.Log +import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody @@ -11,10 +12,12 @@ import java.util.concurrent.TimeUnit import javax.net.ssl.SSLContext import javax.net.ssl.X509TrustManager + object HTTP { var isConnectedToNetwork: (() -> Boolean) = { false } private val seedNodeConnection by lazy { + OkHttpClient().newBuilder() .callTimeout(timeout, TimeUnit.SECONDS) .connectTimeout(timeout, TimeUnit.SECONDS) @@ -106,7 +109,7 @@ object HTTP { Verb.GET -> request.get() Verb.PUT, Verb.POST -> { if (body == null) { throw Exception("Invalid request body.") } - val contentType = MediaType.get("application/json; charset=utf-8") + val contentType = "application/json; charset=utf-8".toMediaType() @Suppress("NAME_SHADOWING") val body = RequestBody.create(contentType, body) if (verb == Verb.PUT) request.put(body) else request.post(body) } @@ -114,7 +117,7 @@ object HTTP { } lateinit var response: Response try { - val connection = if (timeout != HTTP.timeout) { // Custom timeout + val connection: OkHttpClient = if (timeout != HTTP.timeout) { // Custom timeout if (useSeedNodeConnection) { throw IllegalStateException("Setting a custom timeout is only allowed for requests to snodes.") } @@ -122,6 +125,7 @@ object HTTP { } else { if (useSeedNodeConnection) seedNodeConnection else defaultConnection } + response = connection.newCall(request.build()).execute() } catch (exception: Exception) { Log.d("Loki", "${verb.rawValue} request to $url failed due to error: ${exception.localizedMessage}.") @@ -131,9 +135,9 @@ object HTTP { // Override the actual error so that we can correctly catch failed requests in OnionRequestAPI throw HTTPRequestFailedException(0, null, "HTTP request failed due to: ${exception.message}") } - return when (val statusCode = response.code()) { + return when (val statusCode = response.code) { 200 -> { - response.body()?.bytes() ?: throw Exception("An error occurred.") + response.body!!.bytes() } else -> { Log.d("Loki", "${verb.rawValue} request to $url failed with status code: $statusCode.") From b510b064cfdb161b223af9984f63777454278efa Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 11 Jul 2024 09:44:17 +1000 Subject: [PATCH 10/10] [SES-1652] Swap video views in calls (#1533) * WIP: swap video views * feat: swap video views * minor fixes * minor fix * minor fix * update libsession-util * Revert "update libsession-util" This reverts commit 0d386e706e78d86147728cccb80636d920006d98. * reverse updating libsession-util * update libsession-util * Tweaking colors to match designs * More theme fixes * WebRTC rework Only using two sinks and swapping between them Reworked the device rotation logic as it didn't work well with pitch ( you could tip the device front to back and the rotation went out of whack, so had to resort to more robust calculation for the device orientation. Had to use a deprecated sensor setting but it's the only one I could use that works. * Video management logic update Rounded corners for floating inset Proper handling of video scaling based on video proportions Proper handling of mirroring logic for floating/fullscreen videos depending on whether they are the user or the remote video and whether the camera is front facing or not * Showing floating video inset only when there is at least one video stream active, hiding it when both are inactive * Rotating controls on rotation * Clean up * Review feedback * OrientationManager The new OrientationManager encapsulate the orientation logic and sends out a mutable state flow * PR feedback --------- Co-authored-by: Ryan Zhao Co-authored-by: Ryan ZHAO <> --- .../securesms/calls/OrientationManager.kt | 87 +++++++++ .../securesms/calls/WebRtcCallActivity.kt | 171 ++++++++++++------ .../securesms/service/WebRtcCallService.kt | 1 + .../org/thoughtcrime/securesms/ui/Themes.kt | 2 +- .../securesms/webrtc/CallManager.kt | 129 +++++++++---- .../securesms/webrtc/CallViewModel.kt | 31 ++-- .../securesms/webrtc/Orientation.kt | 8 + .../securesms/webrtc/PeerConnectionWrapper.kt | 4 +- .../securesms/webrtc/VideoState.kt | 17 ++ .../securesms/webrtc/data/CallUtils.kt | 11 -- .../video/RemoteRotationVideoProxySink.kt | 5 +- .../webrtc/video/RotationVideoSink.kt | 15 +- .../drawable/conversation_view_background.xml | 2 +- .../default_bottom_sheet_background.xml | 2 +- .../drawable/default_dialog_background.xml | 2 +- .../main/res/drawable/dialog_background.xml | 2 +- .../ic_baseline_screen_rotation_alt_24.xml | 10 + app/src/main/res/drawable/ic_clear_data.xml | 2 +- .../main/res/drawable/preference_bottom.xml | 2 +- .../main/res/drawable/preference_middle.xml | 2 +- .../main/res/drawable/preference_single.xml | 2 +- .../drawable/preference_single_no_padding.xml | 2 +- app/src/main/res/drawable/preference_top.xml | 2 +- .../layout/activity_appearance_settings.xml | 4 +- .../res/layout/activity_blocked_contacts.xml | 2 +- app/src/main/res/layout/activity_webrtc.xml | 42 ++++- .../main/res/layout/default_group_chip.xml | 2 +- app/src/main/res/layout/view_conversation.xml | 6 +- .../res/layout/view_search_bottom_bar.xml | 2 +- app/src/main/res/values/attrs.xml | 5 +- app/src/main/res/values/colors.xml | 4 +- app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/styles.xml | 5 +- app/src/main/res/values/themes.xml | 45 ++--- libsession/src/main/res/values/attrs.xml | 3 - libsession/src/main/res/values/colors.xml | 1 - 36 files changed, 434 insertions(+), 199 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/calls/OrientationManager.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/webrtc/Orientation.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/webrtc/VideoState.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/webrtc/data/CallUtils.kt create mode 100644 app/src/main/res/drawable/ic_baseline_screen_rotation_alt_24.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/OrientationManager.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/OrientationManager.kt new file mode 100644 index 0000000000..baae40bcb2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/OrientationManager.kt @@ -0,0 +1,87 @@ +package org.thoughtcrime.securesms.calls + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.provider.Settings +import androidx.core.content.ContextCompat.getSystemService +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity.SENSOR_SERVICE +import org.thoughtcrime.securesms.webrtc.Orientation +import kotlin.math.asin + +class OrientationManager(private val context: Context): SensorEventListener { + private var sensorManager: SensorManager? = null + private var rotationVectorSensor: Sensor? = null + + private val _orientation = MutableStateFlow(Orientation.UNKNOWN) + val orientation: StateFlow = _orientation + + fun startOrientationListener(){ + // create the sensor manager if it's still null + if(sensorManager == null) { + sensorManager = context.getSystemService(SENSOR_SERVICE) as SensorManager + } + + if(rotationVectorSensor == null) { + rotationVectorSensor = sensorManager!!.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) + } + + sensorManager?.registerListener(this, rotationVectorSensor, SensorManager.SENSOR_DELAY_UI) + } + + fun stopOrientationListener(){ + sensorManager?.unregisterListener(this) + } + + fun destroy(){ + stopOrientationListener() + sensorManager = null + rotationVectorSensor = null + _orientation.value = Orientation.UNKNOWN + } + + override fun onSensorChanged(event: SensorEvent) { + if (event.sensor.type == Sensor.TYPE_ROTATION_VECTOR) { + // if auto-rotate is off, bail and send UNKNOWN + if (!isAutoRotateOn()) { + _orientation.value = Orientation.UNKNOWN + return + } + + // Get the quaternion from the rotation vector sensor + val quaternion = FloatArray(4) + SensorManager.getQuaternionFromVector(quaternion, event.values) + + // Calculate Euler angles from the quaternion + val pitch = asin(2.0 * (quaternion[0] * quaternion[2] - quaternion[3] * quaternion[1])) + + // Convert radians to degrees + val pitchDegrees = Math.toDegrees(pitch).toFloat() + + // Determine the device's orientation based on the pitch and roll values + val currentOrientation = when { + pitchDegrees > 45 -> Orientation.LANDSCAPE + pitchDegrees < -45 -> Orientation.REVERSED_LANDSCAPE + else -> Orientation.PORTRAIT + } + + if (currentOrientation != _orientation.value) { + _orientation.value = currentOrientation + } + } + } + + //Function to check if Android System Auto-rotate is on or off + private fun isAutoRotateOn(): Boolean { + return Settings.System.getInt( + context.contentResolver, + Settings.System.ACCELEROMETER_ROTATION, 0 + ) == 1 + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt index d46321863b..acc92c3c94 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt @@ -5,11 +5,17 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.graphics.Outline +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorManager import android.media.AudioManager import android.os.Build import android.os.Bundle +import android.provider.Settings import android.view.MenuItem -import android.view.OrientationEventListener +import android.view.View +import android.view.ViewOutlineProvider import android.view.WindowManager import androidx.activity.viewModels import androidx.core.content.ContextCompat @@ -21,7 +27,6 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.isActive -import android.provider.Settings import kotlinx.coroutines.launch import network.loki.messenger.R import network.loki.messenger.databinding.ActivityWebrtcBinding @@ -43,8 +48,10 @@ import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_OUTGOING import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_PRE_INIT import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_RECONNECTING import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_RINGING +import org.thoughtcrime.securesms.webrtc.Orientation import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.EARPIECE import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.SPEAKER_PHONE +import kotlin.math.asin @AndroidEntryPoint class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { @@ -71,16 +78,13 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { } private var hangupReceiver: BroadcastReceiver? = null - private val rotationListener by lazy { - object : OrientationEventListener(this) { - override fun onOrientationChanged(orientation: Int) { - if ((orientation + 15) % 90 < 30) { - viewModel.deviceRotation = orientation -// updateControlsRotation(orientation.quadrantRotation() * -1) - } - } - } - } + /** + * We need to track the device's orientation so we can calculate whether or not to rotate the video streams + * This works a lot better than using `OrientationEventListener > onOrientationChanged' + * which gives us a rotation angle that doesn't take into account pitch vs roll, so tipping the device from front to back would + * trigger the video rotation logic, while we really only want it when the device is in portrait or landscape. + */ + private var orientationManager = OrientationManager(this) override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { @@ -102,13 +106,6 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { super.onCreate(savedInstanceState, ready) - // Only enable auto-rotate if system auto-rotate is enabled - if (isAutoRotateOn()) { - rotationListener.enable() - } else { - rotationListener.disable() - } - binding = ActivityWebrtcBinding.inflate(layoutInflater) setContentView(binding.root) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { @@ -136,6 +133,10 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { supportActionBar?.setDisplayHomeAsUpEnabled(false) } + binding.floatingRendererContainer.setOnClickListener { + viewModel.swapVideos() + } + binding.microphoneButton.setOnClickListener { val audioEnabledIntent = WebRtcCallService.microphoneIntent(this, !viewModel.microphoneEnabled) @@ -174,7 +175,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { Permissions.with(this) .request(Manifest.permission.CAMERA) .onAllGranted { - val intent = WebRtcCallService.cameraEnabled(this, !viewModel.videoEnabled) + val intent = WebRtcCallService.cameraEnabled(this, !viewModel.videoState.value.userVideoEnabled) startService(intent) } .execute() @@ -191,14 +192,44 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { onBackPressed() } + lifecycleScope.launch { + orientationManager.orientation.collect { orientation -> + viewModel.deviceOrientation = orientation + updateControlsRotation() + } + } + + clipFloatingInsets() } - //Function to check if Android System Auto-rotate is on or off - private fun isAutoRotateOn(): Boolean { - return Settings.System.getInt( - contentResolver, - Settings.System.ACCELEROMETER_ROTATION, 0 - ) == 1 + /** + * Makes sure the floating video inset has clipped rounded corners, included with the video stream itself + */ + private fun clipFloatingInsets() { + // clip the video inset with rounded corners + val videoInsetProvider = object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + // all corners + outline.setRoundRect( + 0, 0, view.width, view.height, + resources.getDimensionPixelSize(R.dimen.video_inset_radius).toFloat() + ) + } + } + + binding.floatingRendererContainer.outlineProvider = videoInsetProvider + binding.floatingRendererContainer.clipToOutline = true + } + + override fun onResume() { + super.onResume() + orientationManager.startOrientationListener() + + } + + override fun onPause() { + super.onPause() + orientationManager.stopOrientationListener() } override fun onDestroy() { @@ -206,7 +237,8 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { hangupReceiver?.let { receiver -> LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver) } - rotationListener.disable() + + orientationManager.destroy() } private fun answerCall() { @@ -214,15 +246,31 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { ContextCompat.startForegroundService(this, answerIntent) } - private fun updateControlsRotation(newRotation: Int) { + private fun updateControlsRotation() { with (binding) { - val rotation = newRotation.toFloat() - remoteRecipient.rotation = rotation - speakerPhoneButton.rotation = rotation - microphoneButton.rotation = rotation - enableCameraButton.rotation = rotation - switchCameraButton.rotation = rotation - endCallButton.rotation = rotation + val rotation = when(viewModel.deviceOrientation){ + Orientation.LANDSCAPE -> -90f + Orientation.REVERSED_LANDSCAPE -> 90f + else -> 0f + } + + remoteRecipient.animate().cancel() + remoteRecipient.animate().rotation(rotation).start() + + speakerPhoneButton.animate().cancel() + speakerPhoneButton.animate().rotation(rotation).start() + + microphoneButton.animate().cancel() + microphoneButton.animate().rotation(rotation).start() + + enableCameraButton.animate().cancel() + enableCameraButton.animate().rotation(rotation).start() + + switchCameraButton.animate().cancel() + switchCameraButton.animate().rotation(rotation).start() + + endCallButton.animate().cancel() + endCallButton.animate().rotation(rotation).start() } } @@ -346,34 +394,43 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { } } + // handle video state launch { - viewModel.localVideoEnabledState.collect { isEnabled -> - binding.localRenderer.removeAllViews() - if (isEnabled) { - viewModel.localRenderer?.let { surfaceView -> - surfaceView.setZOrderOnTop(true) + viewModel.videoState.collect { state -> + binding.floatingRenderer.removeAllViews() + binding.fullscreenRenderer.removeAllViews() - // Mirror the video preview of the person making the call to prevent disorienting them - surfaceView.setMirror(true) + // the floating video inset (empty or not) should be shown + // the moment we have either of the video streams + val showFloatingContainer = state.userVideoEnabled || state.remoteVideoEnabled + binding.floatingRendererContainer.isVisible = showFloatingContainer + binding.swapViewIcon.isVisible = showFloatingContainer - binding.localRenderer.addView(surfaceView) + // handle fullscreen video window + if(state.showFullscreenVideo()){ + viewModel.fullscreenRenderer?.let { surfaceView -> + binding.fullscreenRenderer.addView(surfaceView) + binding.fullscreenRenderer.isVisible = true + binding.remoteRecipient.isVisible = false } + } else { + binding.fullscreenRenderer.isVisible = false + binding.remoteRecipient.isVisible = true } - binding.localRenderer.isVisible = isEnabled - binding.enableCameraButton.isSelected = isEnabled - } - } - launch { - viewModel.remoteVideoEnabledState.collect { isEnabled -> - binding.remoteRenderer.removeAllViews() - if (isEnabled) { - viewModel.remoteRenderer?.let { surfaceView -> - binding.remoteRenderer.addView(surfaceView) + // handle floating video window + if(state.showFloatingVideo()){ + viewModel.floatingRenderer?.let { surfaceView -> + binding.floatingRenderer.addView(surfaceView) + binding.floatingRenderer.isVisible = true + binding.swapViewIcon.bringToFront() } + } else { + binding.floatingRenderer.isVisible = false } - binding.remoteRenderer.isVisible = isEnabled - binding.remoteRecipient.isVisible = !isEnabled + + // handle buttons + binding.enableCameraButton.isSelected = state.userVideoEnabled } } } @@ -388,7 +445,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { override fun onStop() { super.onStop() uiJob?.cancel() - binding.remoteRenderer.removeAllViews() - binding.localRenderer.removeAllViews() + binding.fullscreenRenderer.removeAllViews() + binding.floatingRenderer.removeAllViews() } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt index cfe1f38f58..36106123a8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.kt @@ -81,6 +81,7 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener { const val EXTRA_RECIPIENT_ADDRESS = "RECIPIENT_ID" const val EXTRA_ENABLED = "ENABLED" const val EXTRA_AUDIO_COMMAND = "AUDIO_COMMAND" + const val EXTRA_SWAPPED = "is_video_swapped" const val EXTRA_MUTE = "mute_value" const val EXTRA_AVAILABLE = "enabled_value" const val EXTRA_REMOTE_DESCRIPTION = "remote_description" diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt index 3fa861fb71..c1ac53baa8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt @@ -34,7 +34,7 @@ fun AppTheme( ) { val extraColors = LocalContext.current.run { ExtraColors( - settingsBackground = getColorFromTheme(R.attr.colorSettingsBackground), + settingsBackground = getColorFromTheme(R.attr.backgroundSecondary), prominentButtonColor = getColorFromTheme(R.attr.prominentButtonColor), ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt index ff5e481895..52cc0ad322 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt @@ -6,11 +6,14 @@ import android.telephony.TelephonyManager import androidx.core.content.ContextCompat import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.boolean import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put import nl.komponents.kovenant.Promise import nl.komponents.kovenant.functional.bind @@ -51,6 +54,7 @@ import org.webrtc.MediaStream import org.webrtc.PeerConnection import org.webrtc.PeerConnection.IceConnectionState import org.webrtc.PeerConnectionFactory +import org.webrtc.RendererCommon import org.webrtc.RtpReceiver import org.webrtc.SessionDescription import org.webrtc.SurfaceViewRenderer @@ -105,10 +109,15 @@ class CallManager( private val _audioEvents = MutableStateFlow(AudioEnabled(false)) val audioEvents = _audioEvents.asSharedFlow() - private val _videoEvents = MutableStateFlow(VideoEnabled(false)) - val videoEvents = _videoEvents.asSharedFlow() - private val _remoteVideoEvents = MutableStateFlow(VideoEnabled(false)) - val remoteVideoEvents = _remoteVideoEvents.asSharedFlow() + + private val _videoState: MutableStateFlow = MutableStateFlow( + VideoState( + swapped = false, + userVideoEnabled = false, + remoteVideoEnabled = false + ) + ) + val videoState = _videoState private val stateProcessor = StateProcessor(CallState.Idle) @@ -151,9 +160,9 @@ class CallManager( private val outgoingIceDebouncer = Debouncer(200L) - var localRenderer: SurfaceViewRenderer? = null + var floatingRenderer: SurfaceViewRenderer? = null var remoteRotationSink: RemoteRotationVideoProxySink? = null - var remoteRenderer: SurfaceViewRenderer? = null + var fullscreenRenderer: SurfaceViewRenderer? = null private var peerConnectionFactory: PeerConnectionFactory? = null fun clearPendingIceUpdates() { @@ -216,20 +225,18 @@ class CallManager( Util.runOnMainSync { val base = EglBase.create() eglBase = base - localRenderer = SurfaceViewRenderer(context).apply { -// setScalingType(SCALE_ASPECT_FIT) - } + floatingRenderer = SurfaceViewRenderer(context) + floatingRenderer?.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) + + fullscreenRenderer = SurfaceViewRenderer(context) + fullscreenRenderer?.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) - remoteRenderer = SurfaceViewRenderer(context).apply { -// setScalingType(SCALE_ASPECT_FIT) - } remoteRotationSink = RemoteRotationVideoProxySink() - localRenderer?.init(base.eglBaseContext, null) - localRenderer?.setMirror(localCameraState.activeDirection == CameraState.Direction.FRONT) - remoteRenderer?.init(base.eglBaseContext, null) - remoteRotationSink!!.setSink(remoteRenderer!!) + floatingRenderer?.init(base.eglBaseContext, null) + fullscreenRenderer?.init(base.eglBaseContext, null) + remoteRotationSink!!.setSink(fullscreenRenderer!!) val encoderFactory = DefaultVideoEncoderFactory(base.eglBaseContext, true, true) val decoderFactory = DefaultVideoDecoderFactory(base.eglBaseContext) @@ -363,7 +370,8 @@ class CallManager( val byteArray = ByteArray(buffer.data.remaining()) { buffer.data[it] } val json = Json.parseToJsonElement(byteArray.decodeToString()) as JsonObject if (json.containsKey("video")) { - _remoteVideoEvents.value = VideoEnabled((json["video"] as JsonPrimitive).boolean) + _videoState.update { it.copy(remoteVideoEnabled = json["video"]?.jsonPrimitive?.boolean ?: false) } + handleMirroring() } else if (json.containsKey("hangup")) { peerConnectionObservers.forEach(WebRtcListener::onHangup) } @@ -383,13 +391,13 @@ class CallManager( peerConnection?.dispose() peerConnection = null - localRenderer?.release() + floatingRenderer?.release() remoteRotationSink?.release() - remoteRenderer?.release() + fullscreenRenderer?.release() eglBase?.release() - localRenderer = null - remoteRenderer = null + floatingRenderer = null + fullscreenRenderer = null eglBase = null localCameraState = CameraState.UNKNOWN @@ -399,8 +407,11 @@ class CallManager( pendingOffer = null callStartTime = -1 _audioEvents.value = AudioEnabled(false) - _videoEvents.value = VideoEnabled(false) - _remoteVideoEvents.value = VideoEnabled(false) + _videoState.value = VideoState( + swapped = false, + userVideoEnabled = false, + remoteVideoEnabled = false + ) pendingOutgoingIceUpdates.clear() pendingIncomingIceUpdates.clear() } @@ -411,7 +422,7 @@ class CallManager( // If the camera we've switched to is the front one then mirror it to match what someone // would see when looking in the mirror rather than the left<-->right flipped version. - localRenderer?.setMirror(localCameraState.activeDirection == CameraState.Direction.FRONT) + handleMirroring() } fun onPreOffer(callId: UUID, recipient: Recipient, onSuccess: () -> Unit) { @@ -469,7 +480,7 @@ class CallManager( val recipient = recipient ?: return Promise.ofFail(NullPointerException("recipient is null")) val offer = pendingOffer ?: return Promise.ofFail(NullPointerException("pendingOffer is null")) val factory = peerConnectionFactory ?: return Promise.ofFail(NullPointerException("peerConnectionFactory is null")) - val local = localRenderer ?: return Promise.ofFail(NullPointerException("localRenderer is null")) + val local = floatingRenderer ?: return Promise.ofFail(NullPointerException("localRenderer is null")) val base = eglBase ?: return Promise.ofFail(NullPointerException("eglBase is null")) val connection = PeerConnectionWrapper( context, @@ -515,7 +526,7 @@ class CallManager( ?: return Promise.ofFail(NullPointerException("recipient is null")) val factory = peerConnectionFactory ?: return Promise.ofFail(NullPointerException("peerConnectionFactory is null")) - val local = localRenderer + val local = floatingRenderer ?: return Promise.ofFail(NullPointerException("localRenderer is null")) val base = eglBase ?: return Promise.ofFail(NullPointerException("eglBase is null")) @@ -609,13 +620,58 @@ class CallManager( } } + fun swapVideos() { + // update the state + _videoState.update { it.copy(swapped = !it.swapped) } + handleMirroring() + + if (_videoState.value.swapped) { + peerConnection?.rotationVideoSink?.setSink(fullscreenRenderer) + floatingRenderer?.let{remoteRotationSink?.setSink(it) } + } else { + peerConnection?.rotationVideoSink?.apply { + setSink(floatingRenderer) + } + fullscreenRenderer?.let{ remoteRotationSink?.setSink(it) } + } + } + fun handleSetMuteAudio(muted: Boolean) { _audioEvents.value = AudioEnabled(!muted) peerConnection?.setAudioEnabled(!muted) } + /** + * Returns the renderer currently showing the user's video, not the contact's + */ + private fun getUserRenderer() = if(_videoState.value.swapped) fullscreenRenderer else floatingRenderer + + /** + * Returns the renderer currently showing the contact's video, not the user's + */ + private fun getRemoteRenderer() = if(_videoState.value.swapped) floatingRenderer else fullscreenRenderer + + /** + * Makes sure the user's renderer applies mirroring if necessary + */ + private fun handleMirroring() { + val videoState = _videoState.value + + // if we have user video and the camera is front facing, make sure to mirror stream + if(videoState.userVideoEnabled) { + getUserRenderer()?.setMirror(isCameraFrontFacing()) + } + + // the remote video is never mirrored + if(videoState.remoteVideoEnabled){ + getRemoteRenderer()?.setMirror(false) + } + } + fun handleSetMuteVideo(muted: Boolean, lockManager: LockManager) { - _videoEvents.value = VideoEnabled(!muted) + _videoState.update { it.copy(userVideoEnabled = !muted) } + handleMirroring() + val connection = peerConnection ?: return connection.setVideoEnabled(!muted) dataChannel?.let { channel -> @@ -651,9 +707,18 @@ class CallManager( } } - fun setDeviceRotation(newRotation: Int) { - peerConnection?.setDeviceRotation(newRotation) - remoteRotationSink?.rotation = newRotation + fun setDeviceOrientation(orientation: Orientation) { + // set rotation to the video based on the device's orientation and the camera facing direction + val rotation = when (orientation) { + Orientation.PORTRAIT -> 0 + Orientation.LANDSCAPE -> if (isCameraFrontFacing()) 90 else -90 + Orientation.REVERSED_LANDSCAPE -> 270 + else -> 0 + } + + // apply the rotation to the streams + peerConnection?.setDeviceRotation(rotation) + remoteRotationSink?.rotation = rotation } fun handleWiredHeadsetChanged(present: Boolean) { @@ -721,7 +786,7 @@ class CallManager( connection.setCommunicationMode() setAudioEnabled(true) dataChannel?.let { channel -> - val toSend = if (!_videoEvents.value.isEnabled) VIDEO_DISABLED_JSON else VIDEO_ENABLED_JSON + val toSend = if (_videoState.value.userVideoEnabled) VIDEO_ENABLED_JSON else VIDEO_DISABLED_JSON val buffer = DataChannel.Buffer(ByteBuffer.wrap(toSend.toString().encodeToByteArray()), false) channel.send(buffer) } @@ -750,6 +815,8 @@ class CallManager( fun isInitiator(): Boolean = peerConnection?.isInitiator() == true + fun isCameraFrontFacing() = localCameraState.activeDirection != CameraState.Direction.BACK + interface WebRtcListener: PeerConnection.Observer { fun onHangup() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt index 4f27e5d1ad..f49e2d3333 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt @@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.webrtc import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager @@ -29,16 +31,11 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V UNTRUSTED_IDENTITY, } - val localRenderer: SurfaceViewRenderer? - get() = callManager.localRenderer + val floatingRenderer: SurfaceViewRenderer? + get() = callManager.floatingRenderer - val remoteRenderer: SurfaceViewRenderer? - get() = callManager.remoteRenderer - - private var _videoEnabled: Boolean = false - - val videoEnabled: Boolean - get() = _videoEnabled + val fullscreenRenderer: SurfaceViewRenderer? + get() = callManager.fullscreenRenderer private var _microphoneEnabled: Boolean = true @@ -59,18 +56,13 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V get() = callManager.audioEvents.map { it.isEnabled } .onEach { _microphoneEnabled = it } - val localVideoEnabledState - get() = callManager.videoEvents - .map { it.isEnabled } - .onEach { _videoEnabled = it } + val videoState: StateFlow + get() = callManager.videoState - val remoteVideoEnabledState - get() = callManager.remoteVideoEvents.map { it.isEnabled } - - var deviceRotation: Int = 0 + var deviceOrientation: Orientation = Orientation.UNKNOWN set(value) { field = value - callManager.setDeviceRotation(value) + callManager.setDeviceOrientation(value) } val currentCallState @@ -85,4 +77,7 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V val callStartTime: Long get() = callManager.callStartTime + fun swapVideos() { + callManager.swapVideos() + } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/Orientation.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/Orientation.kt new file mode 100644 index 0000000000..05370fda4a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/Orientation.kt @@ -0,0 +1,8 @@ +package org.thoughtcrime.securesms.webrtc + +enum class Orientation { + PORTRAIT, + LANDSCAPE, + REVERSED_LANDSCAPE, + UNKNOWN +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt index f78b93d6b9..b61edbb6d2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt @@ -41,7 +41,7 @@ class PeerConnectionWrapper(private val context: Context, private val mediaStream: MediaStream private val videoSource: VideoSource? private val videoTrack: VideoTrack? - private val rotationVideoSink = RotationVideoSink() + public val rotationVideoSink = RotationVideoSink() val readyForIce get() = peerConnection?.localDescription != null && peerConnection?.remoteDescription != null @@ -103,7 +103,7 @@ class PeerConnectionWrapper(private val context: Context, context, rotationVideoSink ) - rotationVideoSink.mirrored = newCamera.activeDirection == CameraState.Direction.FRONT + rotationVideoSink.setSink(localRenderer) newVideoTrack.setEnabled(false) mediaStream.addTrack(newVideoTrack) diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/VideoState.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/VideoState.kt new file mode 100644 index 0000000000..55bb04038a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/VideoState.kt @@ -0,0 +1,17 @@ +package org.thoughtcrime.securesms.webrtc + +data class VideoState ( + val swapped: Boolean, + val userVideoEnabled: Boolean, + val remoteVideoEnabled: Boolean +){ + fun showFloatingVideo(): Boolean { + return userVideoEnabled && !swapped || + remoteVideoEnabled && swapped + } + + fun showFullscreenVideo(): Boolean { + return userVideoEnabled && swapped || + remoteVideoEnabled && !swapped + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/data/CallUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/data/CallUtils.kt deleted file mode 100644 index dc9f07d051..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/data/CallUtils.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.thoughtcrime.securesms.webrtc.data - -// get the video rotation from a specific rotation, locked into 90 degree -// chunks offset by 45 degrees -fun Int.quadrantRotation() = when (this % 360) { - in 315 .. 360, - in 0 until 45 -> 0 - in 45 until 135 -> 90 - in 135 until 225 -> 180 - else -> 270 -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/RemoteRotationVideoProxySink.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/RemoteRotationVideoProxySink.kt index 2b0caef89c..62b78d6ec8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/RemoteRotationVideoProxySink.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/RemoteRotationVideoProxySink.kt @@ -1,6 +1,6 @@ package org.thoughtcrime.securesms.webrtc.video -import org.thoughtcrime.securesms.webrtc.data.quadrantRotation + import org.webrtc.VideoFrame import org.webrtc.VideoSink @@ -14,8 +14,7 @@ class RemoteRotationVideoProxySink: VideoSink { val thisSink = targetSink ?: return val thisFrame = frame ?: return - val quadrantRotation = rotation.quadrantRotation() - val modifiedRotation = thisFrame.rotation - quadrantRotation + val modifiedRotation = thisFrame.rotation - rotation val newFrame = VideoFrame(thisFrame.buffer, modifiedRotation, thisFrame.timestampNs) thisSink.onFrame(newFrame) diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/RotationVideoSink.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/RotationVideoSink.kt index ec43daf2ef..3522f06f9e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/RotationVideoSink.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/video/RotationVideoSink.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.webrtc.video -import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.webrtc.data.quadrantRotation + import org.webrtc.CapturerObserver import org.webrtc.VideoFrame import org.webrtc.VideoProcessor @@ -12,7 +11,6 @@ import java.util.concurrent.atomic.AtomicBoolean class RotationVideoSink: CapturerObserver, VideoProcessor { var rotation: Int = 0 - var mirrored = false private val capturing = AtomicBoolean(false) private var capturerObserver = SoftReference(null) @@ -31,13 +29,14 @@ class RotationVideoSink: CapturerObserver, VideoProcessor { val observer = capturerObserver.get() if (videoFrame == null || observer == null || !capturing.get()) return - val quadrantRotation = rotation.quadrantRotation() - - val newFrame = VideoFrame(videoFrame.buffer, (videoFrame.rotation + quadrantRotation * if (mirrored && quadrantRotation in listOf(90,270)) -1 else 1) % 360, videoFrame.timestampNs) - val localFrame = VideoFrame(videoFrame.buffer, videoFrame.rotation * if (mirrored && quadrantRotation in listOf(90,270)) -1 else 1, videoFrame.timestampNs) + // cater for frame rotation so that the video is always facing up as we rotate pas a certain point + val newFrame = VideoFrame(videoFrame.buffer, videoFrame.rotation - rotation, videoFrame.timestampNs) + // the frame we are sending to our contact needs to cater for rotation observer.onFrameCaptured(newFrame) - sink.get()?.onFrame(localFrame) + + // the frame we see on the user's phone doesn't require changes + sink.get()?.onFrame(videoFrame) } override fun setSink(sink: VideoSink?) { diff --git a/app/src/main/res/drawable/conversation_view_background.xml b/app/src/main/res/drawable/conversation_view_background.xml index 2f177318e0..50e38698b4 100644 --- a/app/src/main/res/drawable/conversation_view_background.xml +++ b/app/src/main/res/drawable/conversation_view_background.xml @@ -4,6 +4,6 @@ android:color="?android:colorControlHighlight"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/default_bottom_sheet_background.xml b/app/src/main/res/drawable/default_bottom_sheet_background.xml index 63532b0d05..19300aae39 100644 --- a/app/src/main/res/drawable/default_bottom_sheet_background.xml +++ b/app/src/main/res/drawable/default_bottom_sheet_background.xml @@ -3,7 +3,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> - + - + diff --git a/app/src/main/res/drawable/dialog_background.xml b/app/src/main/res/drawable/dialog_background.xml index d607bfc022..e546e1f84c 100644 --- a/app/src/main/res/drawable/dialog_background.xml +++ b/app/src/main/res/drawable/dialog_background.xml @@ -6,6 +6,6 @@ android:insetBottom="16dp"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_screen_rotation_alt_24.xml b/app/src/main/res/drawable/ic_baseline_screen_rotation_alt_24.xml new file mode 100644 index 0000000000..553db9c082 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_screen_rotation_alt_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_clear_data.xml b/app/src/main/res/drawable/ic_clear_data.xml index 320015bb23..84465dd4cb 100644 --- a/app/src/main/res/drawable/ic_clear_data.xml +++ b/app/src/main/res/drawable/ic_clear_data.xml @@ -7,5 +7,5 @@ android:pathData="M19.907,7.674H19.907H4.54H4.54C4.317,7.674 4.095,7.719 3.888,7.806L3.888,7.806C3.681,7.893 3.491,8.023 3.334,8.189C3.176,8.355 3.054,8.554 2.978,8.775L3.922,9.097L2.978,8.775C2.903,8.996 2.877,9.231 2.904,9.465L2.904,9.465L2.904,9.469L4.555,23.412C4.555,23.413 4.555,23.413 4.555,23.414C4.603,23.823 4.807,24.189 5.111,24.447C5.415,24.705 5.798,24.84 6.187,24.84H6.188H18.26H18.26C18.649,24.84 19.032,24.705 19.336,24.447C19.64,24.189 19.844,23.823 19.892,23.414C19.892,23.413 19.892,23.413 19.892,23.412L21.543,9.469L21.544,9.465C21.57,9.231 21.544,8.996 21.469,8.775L21.469,8.775C21.393,8.554 21.271,8.355 21.113,8.189C20.956,8.023 20.766,7.893 20.559,7.806L20.17,8.728L20.559,7.806C20.352,7.719 20.13,7.674 19.907,7.674ZM21.412,1.84H3.031C2.045,1.84 1.149,2.609 1.149,3.674V5.828C1.149,6.893 2.045,7.662 3.031,7.662H21.412C22.398,7.662 23.294,6.893 23.294,5.828V3.674C23.294,2.609 22.398,1.84 21.412,1.84Z" android:strokeWidth="2" android:fillColor="#FF3A3A" - android:strokeColor="?colorPrimaryDark"/> + android:strokeColor="?backgroundSecondary"/> diff --git a/app/src/main/res/drawable/preference_bottom.xml b/app/src/main/res/drawable/preference_bottom.xml index b6c5f506fd..888778b1cb 100644 --- a/app/src/main/res/drawable/preference_bottom.xml +++ b/app/src/main/res/drawable/preference_bottom.xml @@ -6,7 +6,7 @@ android:bottom="@dimen/small_spacing" > - + diff --git a/app/src/main/res/drawable/preference_middle.xml b/app/src/main/res/drawable/preference_middle.xml index bf27aacc72..287645ab83 100644 --- a/app/src/main/res/drawable/preference_middle.xml +++ b/app/src/main/res/drawable/preference_middle.xml @@ -4,7 +4,7 @@ - + - + diff --git a/app/src/main/res/drawable/preference_single_no_padding.xml b/app/src/main/res/drawable/preference_single_no_padding.xml index 252ab0aea3..483894fcc2 100644 --- a/app/src/main/res/drawable/preference_single_no_padding.xml +++ b/app/src/main/res/drawable/preference_single_no_padding.xml @@ -2,7 +2,7 @@ - + diff --git a/app/src/main/res/drawable/preference_top.xml b/app/src/main/res/drawable/preference_top.xml index 8f56ddc870..180aa9f73f 100644 --- a/app/src/main/res/drawable/preference_top.xml +++ b/app/src/main/res/drawable/preference_top.xml @@ -6,7 +6,7 @@ android:top="@dimen/small_spacing" > - + diff --git a/app/src/main/res/layout/activity_appearance_settings.xml b/app/src/main/res/layout/activity_appearance_settings.xml index ce33af82f1..c4886708a3 100644 --- a/app/src/main/res/layout/activity_appearance_settings.xml +++ b/app/src/main/res/layout/activity_appearance_settings.xml @@ -25,7 +25,7 @@ app:cardElevation="0dp" app:cardCornerRadius="@dimen/dialog_corner_radius" android:layout_marginHorizontal="@dimen/medium_spacing" - app:cardBackgroundColor="?colorSettingsBackground" + app:cardBackgroundColor="?backgroundSecondary" android:layout_width="match_parent" android:layout_height="wrap_content"> @@ -111,6 +111,7 @@ android:layout_height="wrap_content"/> + android:layout_width="0dp" + android:background="?backgroundSecondary"> + + android:layout_height="wrap_content" + android:layout_gravity="center"/> + + + + tools:text="8"/> @@ -115,8 +114,7 @@ android:textColor="?unreadIndicatorTextColor" android:textSize="@dimen/very_small_font_size" android:textStyle="bold" - android:text="@" - tools:textColor="?android:textColorPrimary" /> + android:text="@" /> diff --git a/app/src/main/res/layout/view_search_bottom_bar.xml b/app/src/main/res/layout/view_search_bottom_bar.xml index 7c08377a70..51bdd881ff 100644 --- a/app/src/main/res/layout/view_search_bottom_bar.xml +++ b/app/src/main/res/layout/view_search_bottom_bar.xml @@ -61,7 +61,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:padding="8dp" - android:background="?colorPrimaryDark" + android:background="?backgroundSecondary" app:SpinKit_Color="?android:textColorPrimary" android:visibility="gone"/> diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index ae38d0c057..aa356619fe 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -29,6 +29,7 @@ + @@ -56,8 +57,6 @@ - - @@ -97,7 +96,6 @@ - @@ -152,7 +150,6 @@ - diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index eae5a2b167..a427629180 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -43,8 +43,6 @@ #ffbbbbbb #ff808080 #ff595959 - #ff4d4d4d - #ff383838 #0f000000 #26000000 @@ -126,7 +124,7 @@ #31F196 - #111111 + #000000 #1B1B1B #2D2D2D #414141 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 6471f73e52..f43d643b9e 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -32,6 +32,7 @@ 250dp 64dp 8dp + 11dp 4dp 8dp 8dp diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 412eec96db..77140251ec 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -24,8 +24,7 @@ diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 428d37c5ef..eba219b924 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -3,7 +3,7 @@ @@ -316,6 +314,7 @@ @color/classic_dark_6 @color/classic_dark_0 @color/classic_dark_0 + @color/classic_dark_1 ?android:textColorPrimary ?colorAccent @color/classic_dark_6 @@ -325,12 +324,10 @@ @color/gray27 ?colorPrimary @color/navigation_bar - @color/classic_dark_1 @style/Classic.Dark.BottomSheet ?android:textColorPrimary ?actionBarPopupTheme @color/classic_dark_1 - @color/classic_dark_1 @color/classic_dark_3 @color/classic_dark_3 @color/classic_dark_3 @@ -353,10 +350,10 @@ #00000000 @color/classic_dark_1 ?colorCellBackground - @color/classic_dark_2 + @color/classic_dark_1 ?android:textColorSecondary - @color/classic_dark_3 - @color/classic_dark_6 + ?colorAccent + @color/classic_dark_0 @color/classic_dark_2 @@ -368,7 +365,7 @@ @color/classic_dark_0 - @color/classic_dark_3 + @color/classic_dark_2 @color/classic_dark_6 ?colorAccent @color/classic_dark_0 @@ -394,7 +391,7 @@ @color/classic_light_0 @color/classic_light_6 - @color/classic_light_5 + @color/classic_light_5 @color/classic_light_6 ?android:textColorPrimary ?colorAccent @@ -406,7 +403,6 @@ ?colorPrimary @color/classic_light_navigation_bar @color/classic_light_6 - @color/classic_light_5 @color/classic_light_3 @color/classic_light_3 @color/classic_light_3 @@ -441,7 +437,7 @@ ?colorCellBackground @color/classic_light_6 ?android:textColorSecondary - @color/classic_light_3 + ?colorAccent @color/classic_light_0 @color/classic_light_4 @@ -454,7 +450,7 @@ @color/classic_light_6 - @color/classic_light_3 + @color/classic_light_4 @color/classic_light_0 ?colorAccent @color/classic_light_0 @@ -482,6 +478,7 @@ @color/ocean_dark_7 @color/ocean_dark_2 @color/ocean_dark_2 + @color/ocean_dark_1 @color/ocean_dark_7 ?colorAccent @color/ocean_dark_7 @@ -494,7 +491,6 @@ ?colorPrimary ?colorPrimaryDark @color/ocean_dark_3 - @color/ocean_dark_1 @color/ocean_dark_4 @color/ocean_dark_4 @color/ocean_dark_4 @@ -520,7 +516,7 @@ #00000000 @color/ocean_dark_3 ?colorCellBackground - @color/ocean_dark_4 + @color/ocean_dark_3 ?android:textColorSecondary ?colorAccent @color/ocean_dark_0 @@ -565,6 +561,7 @@ @color/ocean_light_1 @color/ocean_light_7 @color/ocean_light_6 + @color/ocean_light_6 @color/ocean_light_1 ?colorAccent @color/ocean_light_1 @@ -577,7 +574,6 @@ @color/ocean_light_7 @color/ocean_light_6 @color/ocean_light_5 - @color/ocean_light_6 @color/ocean_light_3 @color/ocean_light_4 @color/ocean_light_4 @@ -641,7 +637,7 @@ ?input_bar_button_background_opaque_border ?colorAccent ?colorCellBackground - @color/ocean_light_6 + @color/ocean_light_5 ?android:textColorSecondary @color/ocean_light_5 @@ -655,6 +651,7 @@ @color/classic_dark_6 @color/classic_dark_0 @color/classic_dark_0 + @color/classic_dark_1 ?android:textColorPrimary ?colorAccent ?colorAccent @@ -665,12 +662,10 @@ @color/gray27 ?colorPrimary @color/compose_view_background - @color/classic_dark_1 @style/Classic.Dark.BottomSheet ?android:textColorPrimary ?actionBarPopupTheme @color/classic_dark_1 - @color/classic_dark_1 @color/classic_dark_3 @style/Dark.Popup @null @@ -688,10 +683,10 @@ #00000000 @color/classic_dark_1 ?colorCellBackground - @color/classic_dark_2 + @color/classic_dark_1 ?android:textColorSecondary - @color/classic_dark_3 - @color/classic_dark_6 + ?colorAccent + @color/classic_dark_0 @color/classic_dark_2 @@ -703,7 +698,7 @@ @color/classic_dark_0 - @color/classic_dark_3 + @color/classic_dark_2 @color/classic_dark_6 ?colorAccent @color/classic_dark_0 diff --git a/libsession/src/main/res/values/attrs.xml b/libsession/src/main/res/values/attrs.xml index 474fe565e0..a412c13e76 100644 --- a/libsession/src/main/res/values/attrs.xml +++ b/libsession/src/main/res/values/attrs.xml @@ -53,8 +53,6 @@ - - @@ -93,7 +91,6 @@ - diff --git a/libsession/src/main/res/values/colors.xml b/libsession/src/main/res/values/colors.xml index a15aa4163a..9f94120b59 100644 --- a/libsession/src/main/res/values/colors.xml +++ b/libsession/src/main/res/values/colors.xml @@ -40,7 +40,6 @@ #ffbbbbbb #ff808080 #ff595959 - #ff4d4d4d #ff383838 #30000000