mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
Merge remote-tracking branch 'origin/develop' into feature/account-provider-selection
This commit is contained in:
+3
@@ -136,6 +136,9 @@ interface MatrixClient : UrlContentFetcher {
|
||||
/** Scans media content before it is displayed, or `null` when no content scanner is configured for this session. */
|
||||
val contentScanner: ContentScanner?
|
||||
|
||||
/** Whether the session is in the process of shutting down, either because it is being logged out or its cache is being cleared. */
|
||||
val isShuttingDown: Boolean
|
||||
|
||||
/**
|
||||
* Returns the room with the given id if the user has joined it, along with its live [Timeline].
|
||||
* A new instance is created on each call and the caller owns it, so it must be closed once no longer needed.
|
||||
|
||||
+9
@@ -135,6 +135,7 @@ import org.matrix.rustcomponents.sdk.use
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
@@ -304,6 +305,10 @@ class RustMatrixClient(
|
||||
.buffer(Channel.UNLIMITED)
|
||||
.stateIn(sessionCoroutineScope, started = SharingStarted.Eagerly, initialValue = persistentListOf())
|
||||
|
||||
private val _isShuttingDown = AtomicBoolean(false)
|
||||
override val isShuttingDown: Boolean
|
||||
get() = _isShuttingDown.get()
|
||||
|
||||
init {
|
||||
// Make sure the session delegate has a reference to the client to be able to logout on auth error
|
||||
sessionDelegate.bindClient(this)
|
||||
@@ -666,11 +671,13 @@ class RustMatrixClient(
|
||||
}
|
||||
|
||||
override suspend fun clearCache() {
|
||||
_isShuttingDown.set(true)
|
||||
innerClient.clearCaches(innerSyncService)
|
||||
destroy()
|
||||
}
|
||||
|
||||
override suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean) {
|
||||
_isShuttingDown.set(true)
|
||||
sessionCoroutineScope.cancel()
|
||||
// Remove current delegate so we don't receive an auth error
|
||||
clientDelegateTaskHandle?.cancelAndDestroy()
|
||||
@@ -708,6 +715,8 @@ class RustMatrixClient(
|
||||
}
|
||||
|
||||
override suspend fun deactivateAccount(password: String, eraseData: Boolean): Result<Unit> = withContext(sessionDispatcher) {
|
||||
_isShuttingDown.set(true)
|
||||
|
||||
Timber.w("Deactivating account")
|
||||
// Remove current delegate so we don't receive an auth error
|
||||
clientDelegateTaskHandle?.cancelAndDestroy()
|
||||
|
||||
+1
@@ -29,6 +29,7 @@ fun Throwable.mapAuthenticationException(): AuthenticationException {
|
||||
is ClientBuildException.WellKnownLookupFailed -> AuthenticationException.Generic(message)
|
||||
is ClientBuildException.EventCache -> AuthenticationException.Generic(message)
|
||||
is ClientBuildException.InvalidRawKey -> AuthenticationException.Generic(message)
|
||||
is ClientBuildException.WellKnownLookupDisabled -> AuthenticationException.Generic(message)
|
||||
}
|
||||
is OAuthException -> when (this) {
|
||||
is OAuthException.Generic -> AuthenticationException.OAuth(message)
|
||||
|
||||
+20
@@ -8,11 +8,13 @@
|
||||
|
||||
package io.element.android.libraries.matrix.impl.permalink
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.MatrixPatterns
|
||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
@@ -51,6 +53,10 @@ class DefaultPermalinkParser(
|
||||
matrixToConverter.convert(uri) ?: return PermalinkData.FallbackLink(uri)
|
||||
}
|
||||
|
||||
matrixToUri.userIdWithSlash()?.let { userId ->
|
||||
return PermalinkData.UserLink(userId = userId)
|
||||
}
|
||||
|
||||
val result = runCatchingExceptions {
|
||||
parseMatrixEntityFrom(matrixToUri.toString())
|
||||
}.getOrNull()
|
||||
@@ -84,3 +90,17 @@ class DefaultPermalinkParser(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A slash is allowed in the local part of a user id, but it also separates an identifier from an
|
||||
* event id in a permalink, so [parseMatrixEntityFrom] cannot tell the two apart. Only a user id can
|
||||
* start with an `@`, so recover it here rather than letting the ambiguity reach the SDK.
|
||||
*/
|
||||
private fun Uri.userIdWithSlash(): UserId? {
|
||||
val identifier = fragment
|
||||
?.removePrefix("/")
|
||||
?.substringBefore('?')
|
||||
?: return null
|
||||
if (!identifier.startsWith("@") || !identifier.contains('/')) return null
|
||||
return identifier.takeIf { MatrixPatterns.isUserId(it) }?.let(::UserId)
|
||||
}
|
||||
|
||||
+3
-2
@@ -75,6 +75,7 @@ import org.matrix.rustcomponents.sdk.PollData
|
||||
import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle
|
||||
import org.matrix.rustcomponents.sdk.UploadParameters
|
||||
import org.matrix.rustcomponents.sdk.UploadSource
|
||||
import org.matrix.rustcomponents.sdk.createCaptionEdit
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
import timber.log.Timber
|
||||
import uniffi.matrix_sdk.PaginationStatus
|
||||
@@ -341,7 +342,7 @@ class RustTimeline(
|
||||
formattedCaption: String?,
|
||||
): Result<Unit> = withContext(dispatcher) {
|
||||
runCatchingExceptions<Unit> {
|
||||
val editedContent = EditedContent.MediaCaption(
|
||||
val editedContent = createCaptionEdit(
|
||||
caption = caption,
|
||||
formattedCaption = formattedCaption?.let {
|
||||
FormattedBody(body = it, format = MessageFormat.Html)
|
||||
@@ -365,7 +366,7 @@ class RustTimeline(
|
||||
fromNotification: Boolean,
|
||||
msgType: MsgType,
|
||||
): Result<Unit> = withContext(dispatcher) {
|
||||
runCatchingExceptions {
|
||||
runCatchingExceptions<Unit> {
|
||||
val msg = MessageEventContent.from(
|
||||
body = body,
|
||||
htmlBody = htmlBody,
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.permalink
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultPermalinkParserTest : RobolectricTest() {
|
||||
@Test
|
||||
fun `a matrix-to link to a user id with a slash is parsed as a user link`() {
|
||||
val result = createParser().parse("https://matrix.to/#/@it/sme:matrix.org")
|
||||
assertThat(result).isEqualTo(PermalinkData.UserLink(UserId("@it/sme:matrix.org")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an escaped slash in a user id is parsed as a user link`() {
|
||||
val result = createParser().parse("https://matrix.to/#/@it%2Fsme:matrix.org")
|
||||
assertThat(result).isEqualTo(PermalinkData.UserLink(UserId("@it/sme:matrix.org")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an element web link to a user id with a slash is parsed as a user link`() {
|
||||
val result = createParser().parse("https://app.element.io/#/user/@it/sme:matrix.org")
|
||||
assertThat(result).isEqualTo(PermalinkData.UserLink(UserId("@it/sme:matrix.org")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a user id with a slash that is not a valid user id is not a user link`() {
|
||||
val result = createParser().parse("https://example.org/foo/@it/sme")
|
||||
assertThat(result).isInstanceOf(PermalinkData.FallbackLink::class.java)
|
||||
}
|
||||
|
||||
private fun createParser() = DefaultPermalinkParser(
|
||||
matrixToConverter = DefaultMatrixToConverter(),
|
||||
)
|
||||
}
|
||||
+4
@@ -133,6 +133,7 @@ class FakeMatrixClient(
|
||||
private val resetWellKnownConfigLambda: () -> Result<Unit> = { lambdaError() },
|
||||
private val enableAutomaticCallStatusLambda: (Boolean) -> Unit = { },
|
||||
override val contentScanner: ContentScanner? = null,
|
||||
private val isShuttingDownResult: () -> Boolean = { false },
|
||||
) : MatrixClient {
|
||||
var setDisplayNameCalled: Boolean = false
|
||||
private set
|
||||
@@ -305,6 +306,9 @@ class FakeMatrixClient(
|
||||
return knockRoomLambda(roomIdOrAlias, message, serverNames)
|
||||
}
|
||||
|
||||
override val isShuttingDown: Boolean
|
||||
get() = isShuttingDownResult()
|
||||
|
||||
// Mocks
|
||||
|
||||
fun givenCreateRoomResult(result: Result<RoomId>) {
|
||||
|
||||
+18
-4
@@ -62,10 +62,10 @@ class AndroidMediaPreProcessor(
|
||||
/**
|
||||
* Used for calculating `inSampleSize` for bitmaps.
|
||||
*
|
||||
* *Note*: Ideally, this should result in images of up to (but not included) 1280x1280 being sent. However, images with very different width and height
|
||||
* *Note*: Ideally, this should result in images of up to (but not included) 2560x2560 being sent. However, images with very different width and height
|
||||
* values may surpass this limit. (i.e.: an image of `480x3000px` would have `inSampleSize=1` and be sent as is).
|
||||
*/
|
||||
private const val IMAGE_SCALE_REF_SIZE = 640
|
||||
private const val IMAGE_SCALE_REF_SIZE = 1280
|
||||
|
||||
private val notCompressibleImageTypes = listOf(MimeTypes.Gif, MimeTypes.WebP)
|
||||
}
|
||||
@@ -85,8 +85,9 @@ class AndroidMediaPreProcessor(
|
||||
val result = when {
|
||||
mimeType == MimeTypes.Svg -> processSvgImage(uri, mimeType)
|
||||
mimeType.isMimeTypeImage() -> {
|
||||
val shouldBeCompressed = mediaOptimizationConfig.compressImages && mimeType !in notCompressibleImageTypes
|
||||
processImage(uri, mimeType, shouldBeCompressed)
|
||||
val imageMimeType = resolveImageMimeType(uri, mimeType)
|
||||
val shouldBeCompressed = mediaOptimizationConfig.compressImages && imageMimeType !in notCompressibleImageTypes
|
||||
processImage(uri, imageMimeType, shouldBeCompressed)
|
||||
}
|
||||
mimeType.isMimeTypeVideo() -> processVideo(uri, mimeType, mediaOptimizationConfig.videoCompressionPreset)
|
||||
mimeType.isMimeTypeAudio() -> processAudio(uri, mimeType)
|
||||
@@ -155,6 +156,19 @@ class AndroidMediaPreProcessor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The declared mime type can be broader than the image actually is - a wildcard from a share intent, or a mislabelled file - and the encoder is picked
|
||||
* from it. Re-encoding a transparent image as JPEG flattens its alpha channel to black, so resolve the real format from the image header first.
|
||||
*/
|
||||
private fun resolveImageMimeType(uri: Uri, declaredMimeType: String): String {
|
||||
if (declaredMimeType == MimeTypes.Png || declaredMimeType == MimeTypes.Jpeg) return declaredMimeType
|
||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
tryOrNull {
|
||||
contentResolver.openInputStream(uri)?.use { BitmapFactory.decodeStream(it, null, options) }
|
||||
}
|
||||
return options.outMimeType ?: declaredMimeType
|
||||
}
|
||||
|
||||
private suspend fun processImage(uri: Uri, mimeType: String, shouldBeCompressed: Boolean): MediaUploadInfo {
|
||||
Timber.d("Processing image ${uri.path.orEmpty().hash()}")
|
||||
|
||||
|
||||
+54
-7
@@ -70,6 +70,7 @@ class AndroidMediaPreProcessorTest : RobolectricTest() {
|
||||
mediaOptimizationConfig: MediaOptimizationConfig,
|
||||
sdkIntVersion: Int = Build.VERSION_CODES.P,
|
||||
deleteOriginal: Boolean = false,
|
||||
mimeType: String = asset.mimeType,
|
||||
): MediaUploadInfo {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val deleteCallback = lambdaRecorder<Uri?, Unit> {}
|
||||
@@ -81,7 +82,7 @@ class AndroidMediaPreProcessorTest : RobolectricTest() {
|
||||
val file = getFileFromAssets(context, asset.filename)
|
||||
val result = sut.process(
|
||||
uri = file.toUri(),
|
||||
mimeType = asset.mimeType,
|
||||
mimeType = mimeType,
|
||||
deleteOriginal = deleteOriginal,
|
||||
mediaOptimizationConfig = mediaOptimizationConfig,
|
||||
)
|
||||
@@ -91,6 +92,52 @@ class AndroidMediaPreProcessorTest : RobolectricTest() {
|
||||
return data
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a png shared with a wildcard mime type is still encoded as a png`() = runTest {
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetImagePng,
|
||||
mimeType = MimeTypes.Images,
|
||||
mediaOptimizationConfig = MediaOptimizationConfig(
|
||||
compressImages = true,
|
||||
videoCompressionPreset = VideoCompressionPreset.STANDARD,
|
||||
),
|
||||
sdkIntVersion = Build.VERSION_CODES.Q,
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.imageInfo.mimetype).isEqualTo(MimeTypes.Png)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a gif shared with a wildcard mime type is recognised and left uncompressed`() = runTest {
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetAnimatedGif,
|
||||
mimeType = MimeTypes.Images,
|
||||
mediaOptimizationConfig = MediaOptimizationConfig(
|
||||
compressImages = true,
|
||||
videoCompressionPreset = VideoCompressionPreset.STANDARD,
|
||||
),
|
||||
sdkIntVersion = Build.VERSION_CODES.Q,
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.imageInfo.mimetype).isEqualTo(MimeTypes.Gif)
|
||||
assertThat(info.imageInfo.size).isEqualTo(assetAnimatedGif.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a file that is not an image keeps the wildcard mime type it was shared with`() = runTest {
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetText,
|
||||
mimeType = MimeTypes.Images,
|
||||
mediaOptimizationConfig = MediaOptimizationConfig(
|
||||
compressImages = false,
|
||||
videoCompressionPreset = VideoCompressionPreset.STANDARD,
|
||||
),
|
||||
sdkIntVersion = Build.VERSION_CODES.Q,
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.imageInfo.mimetype).isEqualTo(MimeTypes.Images)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Ignore now that min API for enterprise is 33")
|
||||
fun `test processing png`() = runTest {
|
||||
@@ -208,10 +255,10 @@ class AndroidMediaPreProcessorTest : RobolectricTest() {
|
||||
assertThat(info.thumbnailFile).isNotNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = 979,
|
||||
width = 3006,
|
||||
height = 1958,
|
||||
width = 6012,
|
||||
mimetype = MimeTypes.Jpeg,
|
||||
size = 84_845,
|
||||
size = 682_492,
|
||||
ThumbnailInfo(height = 244, width = 751, mimetype = MimeTypes.Jpeg, size = 7_178),
|
||||
thumbnailSource = null,
|
||||
blurhash = "K07gBzX=j_D4xZjoaSe,s:"
|
||||
@@ -233,10 +280,10 @@ class AndroidMediaPreProcessorTest : RobolectricTest() {
|
||||
assertThat(info.thumbnailFile).isNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = 979,
|
||||
width = 3_006,
|
||||
height = 1_958,
|
||||
width = 6_012,
|
||||
mimetype = MimeTypes.Jpeg,
|
||||
size = 84_845,
|
||||
size = 682_492,
|
||||
thumbnailInfo = null,
|
||||
thumbnailSource = null,
|
||||
blurhash = null,
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
## Configuration
|
||||
|
||||
In order to make this module only know about Firebase, the plugin `com.google.gms.google-services` has been disabled from the `app` module.
|
||||
In order to make only this module know about Firebase, the plugin `com.google.gms.google-services` has been disabled from the `app` module.
|
||||
|
||||
To be able to change the values set to `google_app_id` in the file `build.gradle.kts` of this module, you should enable the plugin `com.google.gms.google-services` again, copy the file `google-services.json` to the folder `/app/src/main`, build the project, and check the generated file `app/build/generated/res/google-services/<buildtype>/values/values.xml` to import the generated values into the `build.gradle.kts` files.
|
||||
To be able to change the values set to `google_app_id` in the file `build.gradle.kts` of this module, you should enable the plugin `com.google.gms.google-services` again, copy the file `google-services.json` to the folder `/app`, build the project, and check the generated file `app/build/generated/res/google-services/<buildtype>/values/values.xml` to import the generated values into the `firebase.xml` files.
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
import config.BuildTimeConfig
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
@@ -28,27 +27,10 @@ android {
|
||||
buildTypes {
|
||||
getByName("release") {
|
||||
consumerProguardFiles("consumer-proguard-rules.pro")
|
||||
resValue(
|
||||
type = "string",
|
||||
name = "google_app_id",
|
||||
value = BuildTimeConfig.GOOGLE_APP_ID_RELEASE,
|
||||
)
|
||||
}
|
||||
getByName("debug") {
|
||||
resValue(
|
||||
type = "string",
|
||||
name = "google_app_id",
|
||||
value = BuildTimeConfig.GOOGLE_APP_ID_DEBUG,
|
||||
)
|
||||
}
|
||||
register("nightly") {
|
||||
consumerProguardFiles("consumer-proguard-rules.pro")
|
||||
matchingFallbacks += listOf("release")
|
||||
resValue(
|
||||
type = "string",
|
||||
name = "google_app_id",
|
||||
value = BuildTimeConfig.GOOGLE_APP_ID_NIGHTLY,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="google_app_id" translatable="false" tools:ignore="UnusedResources">1:912726360885:android:def0a4e454042e9b00427c</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="google_app_id" translatable="false" tools:ignore="UnusedResources">1:912726360885:android:e17435e0beb0303000427c</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="google_app_id" translatable="false" tools:ignore="UnusedResources">1:912726360885:android:d097de99a4c23d2700427c</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user