diff --git a/app/build.gradle b/app/build.gradle index 979cb2824c..b61e470001 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, @@ -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/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 d08d28a283..e9cbe0c4d9 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/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/conversation/v2/utilities/MentionUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt index efa4e41d54..618616d91d 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().getContactWithAccountID(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/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 a9b238126f..b19bd76aed 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 @@ -92,8 +94,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" @@ -472,7 +472,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 @@ -483,13 +484,14 @@ open class Storage( if (it != name) userProfile.setName(it) } - // 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 @@ -517,12 +519,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) @@ -531,7 +534,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 8f4c6b0fd4..d08644c1bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -35,6 +35,9 @@ import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import androidx.localbroadcastmanager.content.LocalBroadcastManager import dagger.hilt.android.AndroidEntryPoint +import java.io.File +import java.security.SecureRandom +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -47,8 +50,8 @@ 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 @@ -89,14 +92,10 @@ import org.thoughtcrime.securesms.util.BitmapUtil import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.show -import java.io.File -import java.security.SecureRandom -import javax.inject.Inject - -private const val TAG = "SettingsActivity" @AndroidEntryPoint class SettingsActivity : PassphraseRequiredActionBarActivity() { + private val TAG = "SettingsActivity" @Inject lateinit var configFactory: ConfigFactory @@ -253,39 +252,77 @@ 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() } - } - all(promises) 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) - prefs.setProfileAvatarId(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) + prefs.setProfileAvatarId(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) - } 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 } @@ -452,4 +489,4 @@ private fun LocalBroadcastManager.hasPaths(): Flow = callbackFlow { registerReceiver(receiver, IntentFilter("pathsBuilt")) awaitClose { unregisterReceiver(receiver) } -}.onStart { emit(Unit) }.map { OnionRequestAPI.paths.isNotEmpty() } +}.onStart { emit(Unit) }.map { OnionRequestAPI.paths.isNotEmpty() } \ 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/util/ConfigurationMessageUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ConfigurationMessageUtilities.kt index 94dbcf7d1f..f9fa54f916 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/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/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 51c5d37b40..1131fbafba 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -32,6 +32,7 @@ + @@ -59,8 +60,6 @@ - - @@ -100,7 +99,6 @@ - @@ -155,7 +153,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 0d378a3a6b..a45a6e8495 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 9023b0b7a7..0a875be4a7 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -3,7 +3,7 @@ @@ -317,6 +315,7 @@ @color/classic_dark_6 @color/classic_dark_0 @color/classic_dark_0 + @color/classic_dark_1 ?android:textColorPrimary ?colorAccent @color/classic_dark_6 @@ -326,12 +325,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 @@ -357,10 +354,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 @@ -372,7 +369,7 @@ @color/classic_dark_0 - @color/classic_dark_3 + @color/classic_dark_2 @color/classic_dark_6 ?colorAccent @color/classic_dark_0 @@ -398,7 +395,7 @@ @color/classic_light_0 @color/classic_light_6 - @color/classic_light_5 + @color/classic_light_5 @color/classic_light_6 ?android:textColorPrimary ?colorAccent @@ -410,7 +407,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 @@ -448,7 +444,7 @@ ?colorCellBackground @color/classic_light_6 ?android:textColorSecondary - @color/classic_light_3 + ?colorAccent @color/classic_light_0 @color/classic_light_4 @@ -461,7 +457,7 @@ @color/classic_light_6 - @color/classic_light_3 + @color/classic_light_4 @color/classic_light_0 ?colorAccent @color/classic_light_0 @@ -489,6 +485,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 @@ -501,7 +498,6 @@ ?colorPrimary ?colorPrimaryDark @color/ocean_dark_3 - @color/ocean_dark_1 @color/ocean_dark_4 @color/ocean_dark_4 @color/ocean_dark_4 @@ -530,7 +526,7 @@ #00000000 @color/ocean_dark_3 ?colorCellBackground - @color/ocean_dark_4 + @color/ocean_dark_3 ?android:textColorSecondary ?colorAccent @color/ocean_dark_0 @@ -575,6 +571,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 @@ -587,7 +584,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 @@ -654,7 +650,7 @@ ?input_bar_button_background_opaque_border ?colorAccent ?colorCellBackground - @color/ocean_light_6 + @color/ocean_light_5 ?android:textColorSecondary @color/ocean_light_5 @@ -668,6 +664,7 @@ @color/classic_dark_6 @color/classic_dark_0 @color/classic_dark_0 + @color/classic_dark_1 ?android:textColorPrimary ?colorAccent ?colorAccent @@ -678,12 +675,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 @@ -701,10 +696,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 @@ -716,7 +711,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/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/gradle.properties b/gradle.properties index 6f3a80a7fb..f4f6ddca31 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,8 +21,8 @@ googleServicesVersion=4.3.12 kotlinVersion=1.8.21 android.useAndroidX=true appcompatVersion=1.6.1 -coreVersion=1.8.0 composeVersion=1.6.4 +coreVersion=1.13.1 coroutinesVersion=1.6.4 curve25519Version=0.6.0 daggerVersion=2.46.1 @@ -34,7 +34,7 @@ kovenantVersion=3.3.0 lifecycleVersion=2.7.0 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 25bfeea406..0d057e81f1 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 9e056e50bb..4e15a6c98b 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/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 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.")