Merge branch 'od' into on-2

This commit is contained in:
bemusementpark 2024-07-11 15:07:46 +09:30
commit 30ee3946ef
66 changed files with 617 additions and 311 deletions

View File

@ -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"

View File

@ -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> = _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) {}
}

View File

@ -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()
}
}

View File

@ -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<URLSpan>(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")

View File

@ -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"

View File

@ -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();

View File

@ -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) {

View File

@ -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

View File

@ -35,6 +35,5 @@ public class AndroidLogger extends Log.Logger {
}
@Override
public void blockUntilAllWritesFinished() {
}
public void blockUntilAllWritesFinished() { }
}

View File

@ -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 <reified T: Response> getResponseBody(path: String, requestParameters: String): Promise<T, Exception> {
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(

View File

@ -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<Promise<*, Exception>>()
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 )
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))
}
// 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
}

View File

@ -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"

View File

@ -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<Unit, Exception> {
// 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)
}

View File

@ -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()
}

View File

@ -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<VideoState> = 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()
}

View File

@ -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<VideoState>
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()
}
}

View File

@ -0,0 +1,8 @@
package org.thoughtcrime.securesms.webrtc
enum class Orientation {
PORTRAIT,
LANDSCAPE,
REVERSED_LANDSCAPE,
UNKNOWN
}

View File

@ -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)

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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)

View File

@ -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<CapturerObserver>(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?) {

View File

@ -4,6 +4,6 @@
android:color="?android:colorControlHighlight">
<item>
<color android:color="?colorCellBackground" />
<color android:color="?colorPrimary" />
</item>
</ripple>

View File

@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="?dialog_background_color" />
<solid android:color="?backgroundSecondary" />
<corners
android:topLeftRadius="@dimen/dialog_corner_radius"

View File

@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="?attr/dialog_background_color" />
<solid android:color="?backgroundSecondary" />
<corners android:radius="?dialogCornerRadius" />

View File

@ -6,6 +6,6 @@
android:insetBottom="16dp">
<shape android:shape="rectangle">
<corners android:radius="2dp" />
<solid android:color="?dialog_background_color" />
<solid android:color="?backgroundSecondary" />
</shape>
</inset>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M4,7.59l5,-5c0.78,-0.78 2.05,-0.78 2.83,0L20.24,11h-2.83L10.4,4L5.41,9H8v2H2V5h2V7.59zM20,19h2v-6h-6v2h2.59l-4.99,5l-7.01,-7H3.76l8.41,8.41c0.78,0.78 2.05,0.78 2.83,0l5,-5V19z"/>
</vector>

View File

@ -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"/>
</vector>

View File

@ -6,7 +6,7 @@
android:bottom="@dimen/small_spacing"
>
<shape android:shape="rectangle">
<solid android:color="?colorSettingsBackground"/>
<solid android:color="?backgroundSecondary"/>
<corners android:bottomLeftRadius="?preferenceCornerRadius"
android:bottomRightRadius="?preferenceCornerRadius"/>
</shape>

View File

@ -4,7 +4,7 @@
<item android:left="@dimen/medium_spacing"
android:right="@dimen/medium_spacing">
<shape android:shape="rectangle">
<solid android:color="?colorSettingsBackground"/>
<solid android:color="?backgroundSecondary"/>
</shape>
</item>
<item android:gravity="bottom"

View File

@ -6,7 +6,7 @@
android:right="@dimen/medium_spacing"
android:bottom="@dimen/small_spacing">
<shape android:shape="rectangle">
<solid android:color="?colorSettingsBackground"/>
<solid android:color="?backgroundSecondary"/>
<corners android:radius="?preferenceCornerRadius"/>
</shape>
</item>

View File

@ -2,7 +2,7 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="?colorSettingsBackground"/>
<solid android:color="?backgroundSecondary"/>
<corners android:radius="?preferenceCornerRadius"/>
</shape>
</item>

View File

@ -6,7 +6,7 @@
android:top="@dimen/small_spacing"
>
<shape android:shape="rectangle">
<solid android:color="?colorSettingsBackground"/>
<solid android:color="?backgroundSecondary"/>
<corners android:topLeftRadius="?preferenceCornerRadius"
android:topRightRadius="?preferenceCornerRadius"/>
</shape>

View File

@ -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">
<LinearLayout
@ -317,7 +317,7 @@
<androidx.cardview.widget.CardView
app:cardElevation="0dp"
android:elevation="0dp"
app:cardBackgroundColor="?colorSettingsBackground"
app:cardBackgroundColor="?backgroundSecondary"
app:cardCornerRadius="@dimen/dialog_corner_radius"
android:layout_margin="@dimen/medium_spacing"
android:layout_marginBottom="@dimen/massive_spacing"

View File

@ -10,7 +10,7 @@
app:layout_constraintBottom_toTopOf="@+id/unblockButton"
app:cardCornerRadius="?preferenceCornerRadius"
app:cardElevation="0dp"
app:cardBackgroundColor="?colorSettingsBackground"
app:cardBackgroundColor="?backgroundSecondary"
android:layout_marginHorizontal="@dimen/medium_spacing"
android:layout_marginVertical="@dimen/large_spacing"
android:layout_width="match_parent"

View File

@ -8,7 +8,7 @@
xmlns:tools="http://schemas.android.com/tools">
<FrameLayout
android:id="@+id/remote_parent"
android:id="@+id/fullscreen_renderer_container"
android:background="@color/black"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -18,17 +18,17 @@
app:layout_constraintTop_toTopOf="parent">
<FrameLayout
android:id="@+id/remote_renderer"
android:id="@+id/fullscreen_renderer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
</FrameLayout>
<ImageView
android:id="@+id/remote_recipient"
app:layout_constraintStart_toStartOf="@id/remote_parent"
app:layout_constraintEnd_toEndOf="@id/remote_parent"
app:layout_constraintTop_toTopOf="@id/remote_parent"
app:layout_constraintBottom_toBottomOf="@id/remote_parent"
app:layout_constraintStart_toStartOf="@id/fullscreen_renderer_container"
app:layout_constraintEnd_toEndOf="@id/fullscreen_renderer_container"
app:layout_constraintTop_toTopOf="@id/fullscreen_renderer_container"
app:layout_constraintBottom_toBottomOf="@id/fullscreen_renderer_container"
app:layout_constraintVertical_bias="0.4"
android:layout_width="@dimen/extra_large_profile_picture_size"
android:layout_height="@dimen/extra_large_profile_picture_size"/>
@ -111,6 +111,7 @@
android:layout_height="wrap_content"/>
<FrameLayout
android:id="@+id/floating_renderer_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintDimensionRatio="h,9:16"
@ -118,12 +119,21 @@
android:layout_marginVertical="@dimen/massive_spacing"
app:layout_constraintWidth_percent="0.2"
android:layout_height="0dp"
android:layout_width="0dp">
android:layout_width="0dp"
android:background="?backgroundSecondary">
<ImageView
android:id="@+id/videocam_off_icon"
android:src="@drawable/ic_baseline_videocam_off_24"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center"
app:tint="?android:textColorPrimary"/>
<FrameLayout
android:elevation="8dp"
android:id="@+id/local_renderer"
android:id="@+id/floating_renderer"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
android:layout_height="wrap_content"
android:layout_gravity="center"/>
<com.github.ybq.android.spinkit.SpinKitView
android:id="@+id/local_loading_view"
style="@style/SpinKitView.Large.ThreeBounce"
@ -133,8 +143,20 @@
android:layout_gravity="center"
tools:visibility="visible"
android:visibility="gone" />
</FrameLayout>
<ImageView
android:id="@+id/swap_view_icon"
android:src="@drawable/ic_baseline_screen_rotation_alt_24"
app:layout_constraintTop_toTopOf="@id/floating_renderer_container"
app:layout_constraintEnd_toEndOf="@id/floating_renderer_container"
app:tint="?android:textColorPrimary"
android:layout_marginTop="@dimen/very_small_spacing"
android:layout_marginEnd="@dimen/very_small_spacing"
android:layout_width="14dp"
android:layout_height="14dp"/>
<ImageView
android:id="@+id/endCallButton"
android:background="@drawable/circle_tintable"

View File

@ -10,7 +10,7 @@
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:chipStrokeWidth="1dp"
app:chipStrokeColor="?elementBorderColor"
app:chipBackgroundColor="?dialog_background_color"
app:chipBackgroundColor="?backgroundSecondary"
app:chipMinTouchTargetSize="0dp"
app:chipStartPadding="4dp"
tools:ignore="TouchTargetSizeCheck"

View File

@ -85,8 +85,7 @@
android:textColor="?unreadIndicatorTextColor"
android:textSize="@dimen/very_small_font_size"
android:textStyle="bold"
tools:text="8"
tools:textColor="?android:textColorPrimary" />
tools:text="8"/>
</RelativeLayout>
@ -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="@" />
</RelativeLayout>

View File

@ -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"/>

View File

@ -32,6 +32,7 @@
<attr name="onLightCell" format="reference|color"/>
<attr name="accentColor" format="reference|color"/>
<attr name="backgroundSecondary" format="reference|color"/>
<attr name="prominentButtonColor" format="reference|color"/>
<attr name="elementBorderColor" format="reference|color"/>
<attr name="conversation_background" format="reference|color"/>
@ -59,8 +60,6 @@
<attr name="emoji_tab_strip_background" format="color" />
<attr name="emoji_tab_indicator" format="color" />
<attr name="emoji_tab_underline" format="color" />
<attr name="emoji_tab_seperator" format="color" />
<attr name="emoji_drawer_background" format="color" />
<attr name="emoji_text_color" format="color" />
@ -100,7 +99,6 @@
<attr name="dialog_info_icon" format="reference" />
<attr name="dialog_alert_icon" format="reference" />
<attr name="dialog_background_color" format="reference|color" />
<attr name="conversation_menu_background_color" format="reference|color" />
<attr name="conversation_menu_cell_color" format="reference|color"/>
<attr name="conversation_menu_border_color" format="reference|color"/>
@ -155,7 +153,6 @@
<attr name="default_background_start" format="color|reference"/>
<attr name="default_background_end" format="color|reference"/>
<attr name="colorCellBackground" format="color|reference" />
<attr name="colorSettingsBackground" format="color|reference" />
<attr name="colorDividerBackground" format="color|reference" />
<attr name="outlineButtonBorder" format="color|reference" />
<attr name="outlineButtonText" format="color|reference" />

View File

@ -43,8 +43,6 @@
<color name="gray27">#ffbbbbbb</color>
<color name="gray50">#ff808080</color>
<color name="gray65">#ff595959</color>
<color name="gray70">#ff4d4d4d</color>
<color name="gray78">#ff383838</color>
<color name="transparent_black_6">#0f000000</color>
<color name="transparent_black_15">#26000000</color>
@ -126,7 +124,7 @@
<color name="classic_accent">#31F196</color>
<color name="classic_dark_0">#111111</color>
<color name="classic_dark_0">#000000</color>
<color name="classic_dark_1">#1B1B1B</color>
<color name="classic_dark_2">#2D2D2D</color>
<color name="classic_dark_3">#414141</color>

View File

@ -32,6 +32,7 @@
<dimen name="fake_chat_view_height">250dp</dimen>
<dimen name="setting_button_height">64dp</dimen>
<dimen name="dialog_corner_radius">8dp</dimen>
<dimen name="video_inset_radius">11dp</dimen>
<dimen name="dialog_button_corner_radius">4dp</dimen>
<dimen name="pn_option_corner_radius">8dp</dimen>
<dimen name="path_status_view_size">8dp</dimen>

View File

@ -24,8 +24,7 @@
<style name="ThemeOverlay.Session.AlertDialog" parent="ThemeOverlay.AppCompat.Dialog.Alert">
<item name="android:windowBackground">@drawable/default_dialog_background</item>
<item name="android:colorBackground">?attr/dialog_background_color</item>
<item name="dialog_background_color">?colorPrimary</item>
<item name="android:colorBackground">?backgroundSecondary</item>
<item name="android:colorBackgroundFloating">?colorPrimary</item>
<item name="backgroundTint">?colorPrimary</item>
<item name="android:backgroundDimEnabled">true</item>
@ -266,7 +265,7 @@
</style>
<style name="PopupMenu.MessageRequests" parent="@style/Widget.AppCompat.PopupMenu">
<item name="android:background">?attr/colorSettingsBackground</item>
<item name="android:background">?backgroundSecondary</item>
</style>
</resources>

View File

@ -3,7 +3,7 @@
<style name="Base.Theme.Session" parent="@style/Theme.AppCompat.DayNight">
<item name="actionModeBackground">?colorPrimary</item>
<item name="android:colorBackground">?colorPrimary</item>
<item name="dialog_background_color">?colorPrimary</item>
<item name="backgroundSecondary">@color/classic_dark_1</item>
<item name="theme_preview_incoming">?message_received_background_color</item>
<item name="theme_preview_outgoing">?message_sent_background_color</item>
<item name="theme_preview_background">?colorPrimary</item>
@ -33,7 +33,7 @@
<item name="shapeAppearanceSmallComponent">@style/ShapeAppearance.MaterialComponents.SmallComponent</item>
<item name="elementBorderColor">?android:textColorSecondary</item>
<item name="colorOnSurface">?android:textColorPrimary</item>
<item name="colorSurface">?dialog_background_color</item>
<item name="colorSurface">?backgroundSecondary</item>
<item name="media_overview_toolbar_background">@color/transparent</item>
<item name="media_overview_header_foreground">?android:textColorPrimary</item>
<item name="menu_accept_icon">@drawable/ic_baseline_done_24</item>
@ -193,8 +193,6 @@
<item name="emoji_tab_strip_background">@color/compose_view_background</item>
<item name="emoji_tab_indicator">?colorAccent</item>
<item name="emoji_tab_underline">@color/gray78</item>
<item name="emoji_tab_seperator">@color/gray70</item>
<item name="emoji_drawer_background">@color/compose_text_view_background</item>
<item name="emoji_text_color">@color/white</item>
@ -249,7 +247,7 @@
</style>
<style name="Theme.TextSecure.Dialog.MediaSendProgress" parent="@android:style/Theme.Dialog">
<item name="android:colorBackground">?attr/dialog_background_color</item>
<item name="android:colorBackground">?backgroundSecondary</item>
<item name="android:windowNoTitle">true</item>
</style>
@ -317,6 +315,7 @@
<item name="sessionLogoTint">@color/classic_dark_6</item>
<item name="colorPrimary">@color/classic_dark_0</item>
<item name="colorPrimaryDark">@color/classic_dark_0</item>
<item name="backgroundSecondary">@color/classic_dark_1</item>
<item name="colorControlNormal">?android:textColorPrimary</item>
<item name="colorControlActivated">?colorAccent</item>
<item name="android:textColorPrimary">@color/classic_dark_6</item>
@ -326,12 +325,10 @@
<item name="android:textColorHint">@color/gray27</item>
<item name="android:windowBackground">?colorPrimary</item>
<item name="android:navigationBarColor">@color/navigation_bar</item>
<item name="dialog_background_color">@color/classic_dark_1</item>
<item name="bottomSheetDialogTheme">@style/Classic.Dark.BottomSheet</item>
<item name="actionMenuTextColor">?android:textColorPrimary</item>
<item name="popupTheme">?actionBarPopupTheme</item>
<item name="colorCellBackground">@color/classic_dark_1</item>
<item name="colorSettingsBackground">@color/classic_dark_1</item>
<item name="colorDividerBackground">@color/classic_dark_3</item>
<item name="android:colorControlHighlight">@color/classic_dark_3</item>
<item name="colorControlHighlight">@color/classic_dark_3</item>
@ -357,10 +354,10 @@
<item name="home_gradient_start">#00000000</item>
<item name="home_gradient_end">@color/classic_dark_1</item>
<item name="conversation_pinned_background_color">?colorCellBackground</item>
<item name="conversation_unread_background_color">@color/classic_dark_2</item>
<item name="conversation_unread_background_color">@color/classic_dark_1</item>
<item name="conversation_pinned_icon_color">?android:textColorSecondary</item>
<item name="unreadIndicatorBackgroundColor">@color/classic_dark_3</item>
<item name="unreadIndicatorTextColor">@color/classic_dark_6</item>
<item name="unreadIndicatorBackgroundColor">?colorAccent</item>
<item name="unreadIndicatorTextColor">@color/classic_dark_0</item>
<!-- New conversation button -->
<item name="conversation_color_non_main">@color/classic_dark_2</item>
@ -372,7 +369,7 @@
<item name="conversationMenuSearchBackgroundColor">@color/classic_dark_0</item>
<!-- Conversation -->
<item name="message_received_background_color">@color/classic_dark_3</item>
<item name="message_received_background_color">@color/classic_dark_2</item>
<item name="message_received_text_color">@color/classic_dark_6</item>
<item name="message_sent_background_color">?colorAccent</item>
<item name="message_sent_text_color">@color/classic_dark_0</item>
@ -398,7 +395,7 @@
<!-- Main styles -->
<item name="sessionLogoTint">@color/classic_light_0</item>
<item name="colorPrimary">@color/classic_light_6</item>
<item name="dialog_background_color">@color/classic_light_5</item>
<item name="backgroundSecondary">@color/classic_light_5</item>
<item name="colorPrimaryDark">@color/classic_light_6</item>
<item name="colorControlNormal">?android:textColorPrimary</item>
<item name="colorControlActivated">?colorAccent</item>
@ -410,7 +407,6 @@
<item name="android:windowBackground">?colorPrimary</item>
<item name="android:navigationBarColor">@color/classic_light_navigation_bar</item>
<item name="colorCellBackground">@color/classic_light_6</item>
<item name="colorSettingsBackground">@color/classic_light_5</item>
<item name="colorDividerBackground">@color/classic_light_3</item>
<item name="android:colorControlHighlight">@color/classic_light_3</item>
<item name="colorControlHighlight">@color/classic_light_3</item>
@ -448,7 +444,7 @@
<item name="conversation_pinned_background_color">?colorCellBackground</item>
<item name="conversation_unread_background_color">@color/classic_light_6</item>
<item name="conversation_pinned_icon_color">?android:textColorSecondary</item>
<item name="unreadIndicatorBackgroundColor">@color/classic_light_3</item>
<item name="unreadIndicatorBackgroundColor">?colorAccent</item>
<item name="unreadIndicatorTextColor">@color/classic_light_0</item>
<!-- New conversation button -->
<item name="conversation_color_non_main">@color/classic_light_4</item>
@ -461,7 +457,7 @@
<item name="conversationMenuSearchBackgroundColor">@color/classic_light_6</item>
<!-- Conversation -->
<item name="message_received_background_color">@color/classic_light_3</item>
<item name="message_received_background_color">@color/classic_light_4</item>
<item name="message_received_text_color">@color/classic_light_0</item>
<item name="message_sent_background_color">?colorAccent</item>
<item name="message_sent_text_color">@color/classic_light_0</item>
@ -489,6 +485,7 @@
<item name="sessionLogoTint">@color/ocean_dark_7</item>
<item name="colorPrimary">@color/ocean_dark_2</item>
<item name="colorPrimaryDark">@color/ocean_dark_2</item>
<item name="backgroundSecondary">@color/ocean_dark_1</item>
<item name="colorControlNormal">@color/ocean_dark_7</item>
<item name="colorControlActivated">?colorAccent</item>
<item name="android:textColorPrimary">@color/ocean_dark_7</item>
@ -501,7 +498,6 @@
<item name="default_background_end">?colorPrimary</item>
<item name="default_background_start">?colorPrimaryDark</item>
<item name="colorCellBackground">@color/ocean_dark_3</item>
<item name="colorSettingsBackground">@color/ocean_dark_1</item>
<item name="colorDividerBackground">@color/ocean_dark_4</item>
<item name="android:colorControlHighlight">@color/ocean_dark_4</item>
<item name="colorControlHighlight">@color/ocean_dark_4</item>
@ -530,7 +526,7 @@
<item name="home_gradient_start">#00000000</item>
<item name="home_gradient_end">@color/ocean_dark_3</item>
<item name="conversation_pinned_background_color">?colorCellBackground</item>
<item name="conversation_unread_background_color">@color/ocean_dark_4</item>
<item name="conversation_unread_background_color">@color/ocean_dark_3</item>
<item name="conversation_pinned_icon_color">?android:textColorSecondary</item>
<item name="unreadIndicatorBackgroundColor">?colorAccent</item>
<item name="unreadIndicatorTextColor">@color/ocean_dark_0</item>
@ -575,6 +571,7 @@
<item name="sessionLogoTint">@color/ocean_light_1</item>
<item name="colorPrimary">@color/ocean_light_7</item>
<item name="colorPrimaryDark">@color/ocean_light_6</item>
<item name="backgroundSecondary">@color/ocean_light_6</item>
<item name="colorControlNormal">@color/ocean_light_1</item>
<item name="colorControlActivated">?colorAccent</item>
<item name="android:textColorPrimary">@color/ocean_light_1</item>
@ -587,7 +584,6 @@
<item name="default_background_end">@color/ocean_light_7</item>
<item name="default_background_start">@color/ocean_light_6</item>
<item name="colorCellBackground">@color/ocean_light_5</item>
<item name="colorSettingsBackground">@color/ocean_light_6</item>
<item name="colorDividerBackground">@color/ocean_light_3</item>
<item name="android:colorControlHighlight">@color/ocean_light_4</item>
<item name="colorControlHighlight">@color/ocean_light_4</item>
@ -654,7 +650,7 @@
<item name="scroll_to_bottom_button_border">?input_bar_button_background_opaque_border</item>
<item name="conversation_unread_count_indicator_background">?colorAccent</item>
<item name="conversation_pinned_background_color">?colorCellBackground</item>
<item name="conversation_unread_background_color">@color/ocean_light_6</item>
<item name="conversation_unread_background_color">@color/ocean_light_5</item>
<item name="conversation_pinned_icon_color">?android:textColorSecondary</item>
<item name="message_selected">@color/ocean_light_5</item>
</style>
@ -668,6 +664,7 @@
<item name="sessionLogoTint">@color/classic_dark_6</item>
<item name="colorPrimary">@color/classic_dark_0</item>
<item name="colorPrimaryDark">@color/classic_dark_0</item>
<item name="backgroundSecondary">@color/classic_dark_1</item>
<item name="colorControlNormal">?android:textColorPrimary</item>
<item name="colorControlActivated">?colorAccent</item>
<item name="android:colorControlHighlight">?colorAccent</item>
@ -678,12 +675,10 @@
<item name="android:textColorHint">@color/gray27</item>
<item name="android:windowBackground">?colorPrimary</item>
<item name="android:navigationBarColor">@color/compose_view_background</item>
<item name="dialog_background_color">@color/classic_dark_1</item>
<item name="bottomSheetDialogTheme">@style/Classic.Dark.BottomSheet</item>
<item name="actionMenuTextColor">?android:textColorPrimary</item>
<item name="popupTheme">?actionBarPopupTheme</item>
<item name="colorCellBackground">@color/classic_dark_1</item>
<item name="colorSettingsBackground">@color/classic_dark_1</item>
<item name="colorDividerBackground">@color/classic_dark_3</item>
<item name="actionBarPopupTheme">@style/Dark.Popup</item>
<item name="actionBarWidgetTheme">@null</item>
@ -701,10 +696,10 @@
<item name="home_gradient_start">#00000000</item>
<item name="home_gradient_end">@color/classic_dark_1</item>
<item name="conversation_pinned_background_color">?colorCellBackground</item>
<item name="conversation_unread_background_color">@color/classic_dark_2</item>
<item name="conversation_unread_background_color">@color/classic_dark_1</item>
<item name="conversation_pinned_icon_color">?android:textColorSecondary</item>
<item name="unreadIndicatorBackgroundColor">@color/classic_dark_3</item>
<item name="unreadIndicatorTextColor">@color/classic_dark_6</item>
<item name="unreadIndicatorBackgroundColor">?colorAccent</item>
<item name="unreadIndicatorTextColor">@color/classic_dark_0</item>
<!-- New conversation button -->
<item name="conversation_color_non_main">@color/classic_dark_2</item>
@ -716,7 +711,7 @@
<item name="conversationMenuSearchBackgroundColor">@color/classic_dark_0</item>
<!-- Conversation -->
<item name="message_received_background_color">@color/classic_dark_3</item>
<item name="message_received_background_color">@color/classic_dark_2</item>
<item name="message_received_text_color">@color/classic_dark_6</item>
<item name="message_sent_background_color">?colorAccent</item>
<item name="message_sent_text_color">@color/classic_dark_0</item>

View File

@ -2,6 +2,7 @@
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">127.0.0.1</domain>
<domain includeSubdomains="true">public.loki.foundation</domain>
</domain-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="false">seed1.getsession.org</domain>

View File

@ -21,7 +21,7 @@ googleServicesVersion=4.3.12
kotlinVersion=1.8.21
android.useAndroidX=true
appcompatVersion=1.6.1
coreVersion=1.8.0
coreVersion=1.13.1
composeVersion=1.6.4
coroutinesVersion=1.6.4
curve25519Version=0.6.0
@ -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

View File

@ -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<ByteArray, Exception> {
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)!!)

View File

@ -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)
}

View File

@ -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()

View File

@ -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"
}

View File

@ -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<ConfigurationSyncJob> {

View File

@ -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)

View File

@ -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()

View File

@ -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()

View File

@ -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()
}

View File

@ -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<ByteArray, Exception> {
@ -301,7 +304,7 @@ object OpenGroupApi {
}
private fun send(request: Request): Promise<OnionResponse, Exception> {
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)!!)

View File

@ -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) {

View File

@ -467,9 +467,9 @@ object OnionRequestAPI {
x25519PublicKey: String,
version: Version = Version.V4
): Promise<OnionResponse, Exception> {
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()

View File

@ -9,13 +9,13 @@ import java.util.Locale
internal fun Request.getHeadersForOnionRequest(): Map<String, Any> {
val result = mutableMapOf<String, Any>()
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) {

View File

@ -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)

View File

@ -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 {

View File

@ -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<Runnable?> = 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)
}

View File

@ -53,8 +53,6 @@
<attr name="emoji_tab_strip_background" format="color" />
<attr name="emoji_tab_indicator" format="color" />
<attr name="emoji_tab_underline" format="color" />
<attr name="emoji_tab_seperator" format="color" />
<attr name="emoji_drawer_background" format="color" />
<attr name="emoji_text_color" format="color" />
@ -93,7 +91,6 @@
<attr name="dialog_info_icon" format="reference" />
<attr name="dialog_alert_icon" format="reference" />
<attr name="dialog_background_color" format="reference|color" />
<attr name="conversation_icon_attach_audio" format="reference"/>
<attr name="conversation_icon_attach_video" format="reference" />

View File

@ -40,7 +40,6 @@
<color name="gray27">#ffbbbbbb</color>
<color name="gray50">#ff808080</color>
<color name="gray65">#ff595959</color>
<color name="gray70">#ff4d4d4d</color>
<color name="gray78">#ff383838</color>
<color name="transparent_black_30">#30000000</color>

View File

@ -80,4 +80,7 @@
<string name="clearDevice">Clear Device</string>
<string name="clearDeviceOnly">Clear device only</string>
<string name="clearDeviceAndNetwork">Clear device and network</string>
<string name="profileDisplayPictureRemoveError">Failed to remove display picture.</string>
<string name="profileErrorUpdate">Failed to update profile.</string>
</resources>

View File

@ -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.")