Merge branch 'release/0.91.4'

This commit is contained in:
Benoit Marty 2020-07-06 23:31:28 +02:00
commit 51abdb6066
71 changed files with 471 additions and 436 deletions

View File

@ -1,3 +1,24 @@
Changes in Riot.imX 0.91.4 (2020-XX-XX)
===================================================
Features ✨:
- Re-activate Wellknown support with updated UI (#1614)
Improvements 🙌:
- Upload device keys only once to the homeserver and fix crash when no network (#1629)
Bugfix 🐛:
- Fix crash when coming from a notification (#1601)
- Fix Exception when importing keys (#1576)
- File isn't downloaded when another file with the same name already exists (#1578)
- saved images don't show up in gallery (#1324)
- Fix reply fallback leaking sender locale (#429)
Build 🧱:
- Fix lint false-positive about WorkManager (#1012)
- Upgrade build-tools from 3.5.3 to 3.6.6
- Upgrade gradle from 5.4.1 to 5.6.4
Changes in Riot.imX 0.91.3 (2020-07-01) Changes in Riot.imX 0.91.3 (2020-07-01)
=================================================== ===================================================

View File

@ -10,7 +10,7 @@ buildscript {
} }
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.5.3' classpath 'com.android.tools.build:gradle:3.6.3'
classpath 'com.google.gms:google-services:4.3.2' classpath 'com.google.gms:google-services:4.3.2'
classpath "com.airbnb.okreplay:gradle-plugin:1.5.0" classpath "com.airbnb.okreplay:gradle-plugin:1.5.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

View File

@ -1,6 +1,6 @@
#Fri Sep 27 10:10:35 CEST 2019 #Thu Jul 02 12:33:07 CEST 2020
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

View File

@ -64,3 +64,19 @@
### Webrtc ### Webrtc
-keep class org.webrtc.** { *; } -keep class org.webrtc.** { *; }
### Serializable persisted classes
# https://www.guardsquare.com/en/products/proguard/manual/examples#serializable
-keepnames class * implements java.io.Serializable
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
!static !transient <fields>;
!private <fields>;
!private <methods>;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}

View File

@ -1,5 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="im.vector.matrix.android"> package="im.vector.matrix.android">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@ -8,11 +7,6 @@
<application android:networkSecurityConfig="@xml/network_security_config"> <application android:networkSecurityConfig="@xml/network_security_config">
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
android:exported="false"
tools:node="remove" />
<!-- <!--
The SDK offers a secured File provider to access downloaded files. The SDK offers a secured File provider to access downloaded files.
Access to these file will be given via the FileService, with a temporary Access to these file will be given via the FileService, with a temporary

View File

@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.auth.login
import dagger.Lazy import dagger.Lazy
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.auth.AuthAPI import im.vector.matrix.android.internal.auth.AuthAPI
import im.vector.matrix.android.internal.auth.SessionCreator import im.vector.matrix.android.internal.auth.SessionCreator
@ -27,6 +28,7 @@ import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.network.httpclient.addSocketFactory import im.vector.matrix.android.internal.network.httpclient.addSocketFactory
import im.vector.matrix.android.internal.network.ssl.UnrecognizedCertificateException
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import javax.inject.Inject import javax.inject.Inject
@ -49,13 +51,28 @@ internal class DefaultDirectLoginTask @Inject constructor(
override suspend fun execute(params: DirectLoginTask.Params): Session { override suspend fun execute(params: DirectLoginTask.Params): Session {
val client = buildClient(params.homeServerConnectionConfig) val client = buildClient(params.homeServerConnectionConfig)
val authAPI = retrofitFactory.create(client, params.homeServerConnectionConfig.homeServerUri.toString()) val homeServerUrl = params.homeServerConnectionConfig.homeServerUri.toString()
val authAPI = retrofitFactory.create(client, homeServerUrl)
.create(AuthAPI::class.java) .create(AuthAPI::class.java)
val loginParams = PasswordLoginParams.userIdentifier(params.userId, params.password, params.deviceName) val loginParams = PasswordLoginParams.userIdentifier(params.userId, params.password, params.deviceName)
val credentials = executeRequest<Credentials>(null) { val credentials = try {
apiCall = authAPI.login(loginParams) executeRequest<Credentials>(null) {
apiCall = authAPI.login(loginParams)
}
} catch (throwable: Throwable) {
when (throwable) {
is UnrecognizedCertificateException -> {
throw Failure.UnrecognizedCertificateFailure(
homeServerUrl,
throwable.fingerprint
)
}
else ->
throw throwable
}
} }
return sessionCreator.createSession(credentials, params.homeServerConnectionConfig) return sessionCreator.createSession(credentials, params.homeServerConnectionConfig)

View File

@ -70,7 +70,6 @@ import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldConte
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.model.toRest import im.vector.matrix.android.internal.crypto.model.toRest
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
@ -98,6 +97,7 @@ import im.vector.matrix.android.internal.session.sync.model.SyncResponse
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.TaskThread import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.task.launchToCallback
import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.fetchCopied import im.vector.matrix.android.internal.util.fetchCopied
@ -340,11 +340,14 @@ internal class DefaultCryptoService @Inject constructor(
} }
fun ensureDevice() { fun ensureDevice() {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launchToCallback(coroutineDispatchers.crypto, NoOpMatrixCallback()) {
// Open the store // Open the store
cryptoStore.open() cryptoStore.open()
// TODO why do that everytime? we should mark that it was done // this can throw if no network
uploadDeviceKeys() tryThis {
uploadDeviceKeys()
}
oneTimeKeysUploader.maybeUploadOneTimeKeys() oneTimeKeysUploader.maybeUploadOneTimeKeys()
// this can throw if no backup // this can throw if no backup
tryThis { tryThis {
@ -389,7 +392,7 @@ internal class DefaultCryptoService @Inject constructor(
// } else { // } else {
// Why would we do that? it will be called at end of syn // Why would we do that? it will be called at end of syn
incomingGossipingRequestManager.processReceivedGossipingRequests() incomingGossipingRequestManager.processReceivedGossipingRequests()
// } // }
}.fold( }.fold(
{ {
@ -888,7 +891,7 @@ internal class DefaultCryptoService @Inject constructor(
*/ */
private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean { private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean {
return when (secretName) { return when (secretName) {
MASTER_KEY_SSSS_NAME -> { MASTER_KEY_SSSS_NAME -> {
crossSigningService.onSecretMSKGossip(secretValue) crossSigningService.onSecretMSKGossip(secretValue)
true true
} }
@ -980,7 +983,11 @@ internal class DefaultCryptoService @Inject constructor(
/** /**
* Upload my user's device keys. * Upload my user's device keys.
*/ */
private suspend fun uploadDeviceKeys(): KeysUploadResponse { private suspend fun uploadDeviceKeys() {
if (cryptoStore.getDeviceKeysUploaded()) {
Timber.d("Keys already uploaded, nothing to do")
return
}
// Prepare the device keys data to send // Prepare the device keys data to send
// Sign it // Sign it
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, getMyDevice().signalableJSONDictionary()) val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, getMyDevice().signalableJSONDictionary())
@ -991,7 +998,9 @@ internal class DefaultCryptoService @Inject constructor(
) )
val uploadDeviceKeysParams = UploadKeysTask.Params(rest, null) val uploadDeviceKeysParams = UploadKeysTask.Params(rest, null)
return uploadKeysTask.execute(uploadDeviceKeysParams) uploadKeysTask.execute(uploadDeviceKeysParams)
cryptoStore.setDeviceKeysUploaded(true)
} }
/** /**

View File

@ -433,4 +433,7 @@ internal interface IMXCryptoStore {
fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest?
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
fun getGossipingEventsTrail(): List<Event> fun getGossipingEventsTrail(): List<Event>
fun setDeviceKeysUploaded(uploaded: Boolean)
fun getDeviceKeysUploaded(): Boolean
} }

View File

@ -842,6 +842,18 @@ internal class RealmCryptoStore @Inject constructor(
} ?: false } ?: false
} }
override fun setDeviceKeysUploaded(uploaded: Boolean) {
doRealmTransaction(realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()?.deviceKeysSentToServer = uploaded
}
}
override fun getDeviceKeysUploaded(): Boolean {
return doWithRealm(realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()?.deviceKeysSentToServer
} ?: false
}
override fun setRoomsListBlacklistUnverifiedDevices(roomIds: List<String>) { override fun setRoomsListBlacklistUnverifiedDevices(roomIds: List<String>) {
doRealmTransaction(realmConfiguration) { doRealmTransaction(realmConfiguration) {
// Reset all // Reset all

View File

@ -54,7 +54,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
// 0, 1, 2: legacy Riot-Android // 0, 1, 2: legacy Riot-Android
// 3: migrate to RiotX schema // 3: migrate to RiotX schema
// 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6) // 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
const val CRYPTO_STORE_SCHEMA_VERSION = 10L const val CRYPTO_STORE_SCHEMA_VERSION = 11L
} }
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
@ -70,6 +70,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
if (oldVersion <= 7) migrateTo8(realm) if (oldVersion <= 7) migrateTo8(realm)
if (oldVersion <= 8) migrateTo9(realm) if (oldVersion <= 8) migrateTo9(realm)
if (oldVersion <= 9) migrateTo10(realm) if (oldVersion <= 9) migrateTo10(realm)
if (oldVersion <= 10) migrateTo11(realm)
} }
private fun migrateTo1Legacy(realm: DynamicRealm) { private fun migrateTo1Legacy(realm: DynamicRealm) {
@ -176,13 +177,14 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
} }
} }
// Convert MXOlmInboundGroupSession2 to OlmInboundGroupSessionWrapper2 // Convert MXOlmInboundGroupSession2 to OlmInboundGroupSessionWrapper
realm.schema.get("OlmInboundGroupSessionEntity") realm.schema.get("OlmInboundGroupSessionEntity")
?.transform { obj -> ?.transform { obj ->
try { try {
val oldSerializedData = obj.getString("olmInboundGroupSessionData") val oldSerializedData = obj.getString("olmInboundGroupSessionData")
deserializeFromRealm<MXOlmInboundGroupSession2>(oldSerializedData)?.let { mxOlmInboundGroupSession2 -> deserializeFromRealm<MXOlmInboundGroupSession2>(oldSerializedData)?.let { mxOlmInboundGroupSession2 ->
val newOlmInboundGroupSessionWrapper2 = OlmInboundGroupSessionWrapper2() val sessionKey = mxOlmInboundGroupSession2.mSession.sessionIdentifier()
val newOlmInboundGroupSessionWrapper = OlmInboundGroupSessionWrapper(sessionKey, false)
.apply { .apply {
olmInboundGroupSession = mxOlmInboundGroupSession2.mSession olmInboundGroupSession = mxOlmInboundGroupSession2.mSession
roomId = mxOlmInboundGroupSession2.mRoomId roomId = mxOlmInboundGroupSession2.mRoomId
@ -191,7 +193,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
forwardingCurve25519KeyChain = mxOlmInboundGroupSession2.mForwardingCurve25519KeyChain forwardingCurve25519KeyChain = mxOlmInboundGroupSession2.mForwardingCurve25519KeyChain
} }
obj.setString("olmInboundGroupSessionData", serializeForRealm(newOlmInboundGroupSessionWrapper2)) obj.setString("olmInboundGroupSessionData", serializeForRealm(newOlmInboundGroupSessionWrapper))
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Error") Timber.e(e, "Error")
@ -445,4 +447,11 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
.addField(SharedSessionEntityFields.CHAIN_INDEX, Long::class.java) .addField(SharedSessionEntityFields.CHAIN_INDEX, Long::class.java)
.setNullable(SharedSessionEntityFields.CHAIN_INDEX, true) .setNullable(SharedSessionEntityFields.CHAIN_INDEX, true)
} }
// Version 11L added deviceKeysSentToServer boolean to CryptoMetadataEntity
private fun migrateTo11(realm: DynamicRealm) {
Timber.d("Step 10 -> 11")
realm.schema.get("CryptoMetadataEntity")
?.addField(CryptoMetadataEntityFields.DEVICE_KEYS_SENT_TO_SERVER, Boolean::class.java)
}
} }

View File

@ -36,6 +36,9 @@ internal open class CryptoMetadataEntity(
// The keys backup version currently used. Null means no backup. // The keys backup version currently used. Null means no backup.
var backupVersion: String? = null, var backupVersion: String? = null,
// The device keys has been sent to the homeserver
var deviceKeysSentToServer: Boolean = false,
var xSignMasterPrivateKey: String? = null, var xSignMasterPrivateKey: String? = null,
var xSignUserPrivateKey: String? = null, var xSignUserPrivateKey: String? = null,
var xSignSelfSignedPrivateKey: String? = null, var xSignSelfSignedPrivateKey: String? = null,

View File

@ -177,7 +177,6 @@ internal class LocalEchoEventFactory @Inject constructor(
val body = bodyForReply(originalEvent.getLastMessageContent(), originalEvent.root.getClearContent().toModel()) val body = bodyForReply(originalEvent.getLastMessageContent(), originalEvent.root.getClearContent().toModel())
val replyFormatted = REPLY_PATTERN.format( val replyFormatted = REPLY_PATTERN.format(
permalink, permalink,
stringProvider.getString(R.string.message_reply_to_prefix),
userLink, userLink,
originalEvent.senderInfo.disambiguatedDisplayName, originalEvent.senderInfo.disambiguatedDisplayName,
body.takeFormatted(), body.takeFormatted(),
@ -372,7 +371,6 @@ internal class LocalEchoEventFactory @Inject constructor(
val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.root.getClearContent().toModel()) val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.root.getClearContent().toModel())
val replyFormatted = REPLY_PATTERN.format( val replyFormatted = REPLY_PATTERN.format(
permalink, permalink,
stringProvider.getString(R.string.message_reply_to_prefix),
userLink, userLink,
userId, userId,
body.takeFormatted(), body.takeFormatted(),
@ -434,10 +432,10 @@ internal class LocalEchoEventFactory @Inject constructor(
TextContent(content.body, formattedText) TextContent(content.body, formattedText)
} }
} }
MessageType.MSGTYPE_FILE -> return TextContent(stringProvider.getString(R.string.reply_to_a_file)) MessageType.MSGTYPE_FILE -> return TextContent("sent a file.")
MessageType.MSGTYPE_AUDIO -> return TextContent(stringProvider.getString(R.string.reply_to_an_audio_file)) MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.")
MessageType.MSGTYPE_IMAGE -> return TextContent(stringProvider.getString(R.string.reply_to_an_image)) MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.")
MessageType.MSGTYPE_VIDEO -> return TextContent(stringProvider.getString(R.string.reply_to_a_video)) MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.")
else -> return TextContent(content?.body ?: "") else -> return TextContent(content?.body ?: "")
} }
} }
@ -489,6 +487,6 @@ internal class LocalEchoEventFactory @Inject constructor(
// </blockquote> // </blockquote>
// </mx-reply> // </mx-reply>
// No whitespace because currently breaks temporary formatted text to Span // No whitespace because currently breaks temporary formatted text to Span
const val REPLY_PATTERN = """<mx-reply><blockquote><a href="%s">%s</a><a href="%s">%s</a><br />%s</blockquote></mx-reply>%s""" const val REPLY_PATTERN = """<mx-reply><blockquote><a href="%s">In reply to</a> <a href="%s">%s</a><br />%s</blockquote></mx-reply>%s"""
} }
} }

View File

@ -27,6 +27,7 @@ import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.network.httpclient.addSocketFactory import im.vector.matrix.android.internal.network.httpclient.addSocketFactory
import im.vector.matrix.android.internal.network.ssl.UnrecognizedCertificateException
import im.vector.matrix.android.internal.session.homeserver.CapabilitiesAPI import im.vector.matrix.android.internal.session.homeserver.CapabilitiesAPI
import im.vector.matrix.android.internal.session.identity.IdentityAuthAPI import im.vector.matrix.android.internal.session.identity.IdentityAuthAPI
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
@ -106,6 +107,12 @@ internal class DefaultGetWellknownTask @Inject constructor(
} }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
when (throwable) { when (throwable) {
is UnrecognizedCertificateException -> {
throw Failure.UnrecognizedCertificateFailure(
"https://$domain",
throwable.fingerprint
)
}
is Failure.NetworkConnection -> { is Failure.NetworkConnection -> {
WellknownResult.Ignore WellknownResult.Ignore
} }

View File

@ -63,12 +63,6 @@
<string name="summary_user_sent_sticker">أرسل %1$s ملصقا.</string> <string name="summary_user_sent_sticker">أرسل %1$s ملصقا.</string>
<string name="notice_avatar_changed_too">(تغيّرت الصورة أيضا)</string> <string name="notice_avatar_changed_too">(تغيّرت الصورة أيضا)</string>
<string name="message_reply_to_prefix">ردا على</string>
<string name="reply_to_an_image">أرسل صورة.</string>
<string name="reply_to_a_video">أرسل فديوهًا.</string>
<string name="reply_to_an_audio_file">أرسل ملف صوت.</string>
<string name="reply_to_a_file">أرسل ملفًا.</string>
<string name="room_displayname_invite_from">دعوة من %s</string> <string name="room_displayname_invite_from">دعوة من %s</string>
<string name="room_displayname_empty_room">غرفة فارغة</string> <string name="room_displayname_empty_room">غرفة فارغة</string>

View File

@ -52,8 +52,6 @@
<string name="notice_crypto_unable_to_decrypt">** Şifrəni aça bilmir: %s **</string> <string name="notice_crypto_unable_to_decrypt">** Şifrəni aça bilmir: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">Göndərənin cihazı bu mesaj üçün açarları bizə göndərməyib.</string> <string name="notice_crypto_error_unkwown_inbound_session_id">Göndərənin cihazı bu mesaj üçün açarları bizə göndərməyib.</string>
<string name="message_reply_to_prefix">Cavab olaraq</string>
<string name="could_not_redact">Redaktə etmək olmur</string> <string name="could_not_redact">Redaktə etmək olmur</string>
<string name="unable_to_send_message">Mesaj göndərmək olmur</string> <string name="unable_to_send_message">Mesaj göndərmək olmur</string>
@ -69,11 +67,6 @@
<string name="medium_email">Elektron poçt ünvanı</string> <string name="medium_email">Elektron poçt ünvanı</string>
<string name="medium_phone_number">Telefon nömrəsi</string> <string name="medium_phone_number">Telefon nömrəsi</string>
<string name="reply_to_an_image">şəkil göndərdi.</string>
<string name="reply_to_a_video">video göndərdi.</string>
<string name="reply_to_an_audio_file">səs faylı göndərdi.</string>
<string name="reply_to_a_file">fayl göndərdi.</string>
<string name="room_displayname_invite_from">%s-dən dəvət</string> <string name="room_displayname_invite_from">%s-dən dəvət</string>
<string name="room_displayname_room_invite">Otağa dəvət</string> <string name="room_displayname_room_invite">Otağa dəvət</string>

View File

@ -63,13 +63,6 @@
<string name="summary_user_sent_sticker">%1$s изпрати стикер.</string> <string name="summary_user_sent_sticker">%1$s изпрати стикер.</string>
<string name="message_reply_to_prefix">В отговор на</string>
<string name="reply_to_an_image">изпрати снимка.</string>
<string name="reply_to_a_video">изпрати видео.</string>
<string name="reply_to_an_audio_file">изпрати аудио файл.</string>
<string name="reply_to_a_file">изпрати файл.</string>
<string name="room_displayname_invite_from">Покана от %s</string> <string name="room_displayname_invite_from">Покана от %s</string>
<string name="room_displayname_room_invite">Покана за стая</string> <string name="room_displayname_room_invite">Покана за стая</string>
<string name="room_displayname_two_members">%1$s и %2$s</string> <string name="room_displayname_two_members">%1$s и %2$s</string>

View File

@ -76,11 +76,4 @@
<string name="summary_user_sent_sticker">%1$s ha enviat un adhesiu.</string> <string name="summary_user_sent_sticker">%1$s ha enviat un adhesiu.</string>
<string name="message_reply_to_prefix">En resposta a</string>
<string name="reply_to_an_image">ha enviat una imatge.</string>
<string name="reply_to_a_video">ha enviat un vídeo.</string>
<string name="reply_to_an_audio_file">ha enviat un fitxer d\'àudio.</string>
<string name="reply_to_a_file">ha enviat un fitxer.</string>
</resources> </resources>

View File

@ -46,8 +46,6 @@
<string name="notice_crypto_unable_to_decrypt">** Nelze dešifrovat: %s **</string> <string name="notice_crypto_unable_to_decrypt">** Nelze dešifrovat: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">Odesílatelovo zařízení neposlalo klíče pro tuto zprávu.</string> <string name="notice_crypto_error_unkwown_inbound_session_id">Odesílatelovo zařízení neposlalo klíče pro tuto zprávu.</string>
<string name="message_reply_to_prefix">V odpovědi na</string>
<string name="could_not_redact">Nelze vymazat</string> <string name="could_not_redact">Nelze vymazat</string>
<string name="unable_to_send_message">Zprávu nelze odeslat</string> <string name="unable_to_send_message">Zprávu nelze odeslat</string>
@ -63,11 +61,6 @@
<string name="medium_email">E-mailová adresa</string> <string name="medium_email">E-mailová adresa</string>
<string name="medium_phone_number">Telefonní číslo</string> <string name="medium_phone_number">Telefonní číslo</string>
<string name="reply_to_an_image">odeslal obrázek.</string>
<string name="reply_to_a_video">odeslal video.</string>
<string name="reply_to_an_audio_file">odeslal zvukový soubor.</string>
<string name="reply_to_a_file">odeslal soubor.</string>
<string name="room_displayname_invite_from">Pozvání od %s</string> <string name="room_displayname_invite_from">Pozvání od %s</string>
<string name="room_displayname_room_invite">Pozvání do místnosti</string> <string name="room_displayname_room_invite">Pozvání do místnosti</string>

View File

@ -73,13 +73,6 @@
<string name="summary_user_sent_sticker">%1$s sandte einen Sticker.</string> <string name="summary_user_sent_sticker">%1$s sandte einen Sticker.</string>
<string name="message_reply_to_prefix">Als Antwort auf</string>
<string name="reply_to_an_image">hat ein Bild gesendet.</string>
<string name="reply_to_a_video">hat ein Video gesendet.</string>
<string name="reply_to_an_audio_file">hat eine Audio-Datei gesendet.</string>
<string name="reply_to_a_file">sandte eine Datei.</string>
<!-- Room display name --> <!-- Room display name -->
<string name="room_displayname_invite_from">Einladung von %s</string> <string name="room_displayname_invite_from">Einladung von %s</string>
<string name="room_displayname_room_invite">Raumeinladung</string> <string name="room_displayname_room_invite">Raumeinladung</string>

View File

@ -40,8 +40,6 @@
<string name="notice_crypto_unable_to_decrypt">** Αδυναμία αποκρυπτογράφησης: %s **</string> <string name="notice_crypto_unable_to_decrypt">** Αδυναμία αποκρυπτογράφησης: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">Η συσκευή του/της αποστολέα δεν μας έχει στείλει τα κλειδιά για αυτό το μήνυμα.</string> <string name="notice_crypto_error_unkwown_inbound_session_id">Η συσκευή του/της αποστολέα δεν μας έχει στείλει τα κλειδιά για αυτό το μήνυμα.</string>
<string name="message_reply_to_prefix">Προς απάντηση στο</string>
<string name="unable_to_send_message">Αποτυχία αποστολής μηνύματος</string> <string name="unable_to_send_message">Αποτυχία αποστολής μηνύματος</string>
<string name="message_failed_to_upload">Αποτυχία αναφόρτωσης εικόνας</string> <string name="message_failed_to_upload">Αποτυχία αναφόρτωσης εικόνας</string>
@ -56,10 +54,6 @@
<string name="notice_voip_finished">Η VoIP διάσκεψη έληξε</string> <string name="notice_voip_finished">Η VoIP διάσκεψη έληξε</string>
<string name="notice_room_join">Ο/Η %1$s εισήλθε στο δωμάτιο</string> <string name="notice_room_join">Ο/Η %1$s εισήλθε στο δωμάτιο</string>
<string name="reply_to_an_image">έστειλε μία εικόνα.</string>
<string name="reply_to_a_video">έστειλε ένα βίντεο.</string>
<string name="reply_to_an_audio_file">έστειλε ένα αρχείο ήχου.</string>
<string name="reply_to_a_file">έστειλε ένα αρχείο.</string>
<string name="room_displayname_invite_from">Πρόσκληση από %s</string> <string name="room_displayname_invite_from">Πρόσκληση από %s</string>
<string name="room_displayname_room_invite">Πρόσκληση στο δωμάτιο</string> <string name="room_displayname_room_invite">Πρόσκληση στο δωμάτιο</string>

View File

@ -17,8 +17,6 @@
<string name="notice_crypto_unable_to_decrypt">** Ne eblas malĉifri: %s **</string> <string name="notice_crypto_unable_to_decrypt">** Ne eblas malĉifri: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">La aparato de la sendanto ne sendis al ni la ŝlosilojn por tiu mesaĝo.</string> <string name="notice_crypto_error_unkwown_inbound_session_id">La aparato de la sendanto ne sendis al ni la ŝlosilojn por tiu mesaĝo.</string>
<string name="message_reply_to_prefix">Responde al</string>
<string name="summary_message">%1$s: %2$s</string> <string name="summary_message">%1$s: %2$s</string>
<string name="notice_display_name_set">%1$s ŝanĝis sian vidigan nomon al %2$s</string> <string name="notice_display_name_set">%1$s ŝanĝis sian vidigan nomon al %2$s</string>
<string name="notice_display_name_changed_from">%1$s ŝanĝis sian vidigan nomon de %2$s al %3$s</string> <string name="notice_display_name_changed_from">%1$s ŝanĝis sian vidigan nomon de %2$s al %3$s</string>
@ -62,11 +60,6 @@
<string name="medium_email">Retpoŝtadreso</string> <string name="medium_email">Retpoŝtadreso</string>
<string name="medium_phone_number">Telefonnumero</string> <string name="medium_phone_number">Telefonnumero</string>
<string name="reply_to_an_image">sendis bildon.</string>
<string name="reply_to_a_video">sendis filmon.</string>
<string name="reply_to_an_audio_file">sendis sondosieron.</string>
<string name="reply_to_a_file">sendis dosieron.</string>
<string name="room_displayname_invite_from">Invito de %s</string> <string name="room_displayname_invite_from">Invito de %s</string>
<string name="room_displayname_room_invite">Ĉambra invito</string> <string name="room_displayname_room_invite">Ĉambra invito</string>

View File

@ -73,13 +73,6 @@
<string name="summary_user_sent_sticker">%1$s envió una calcomanía.</string> <string name="summary_user_sent_sticker">%1$s envió una calcomanía.</string>
<string name="message_reply_to_prefix">En respuesta a</string>
<string name="reply_to_an_image">envió una imagen.</string>
<string name="reply_to_a_video">envió un video.</string>
<string name="reply_to_an_audio_file">envió un archivo de audio.</string>
<string name="reply_to_a_file">envió un archivo.</string>
<!-- Room display name --> <!-- Room display name -->
<string name="room_displayname_invite_from">Invitación de %s</string> <string name="room_displayname_invite_from">Invitación de %s</string>
<string name="room_displayname_room_invite">Invitación de Sala</string> <string name="room_displayname_room_invite">Invitación de Sala</string>

View File

@ -73,13 +73,6 @@
<string name="summary_user_sent_sticker">%1$s envió una pegatina.</string> <string name="summary_user_sent_sticker">%1$s envió una pegatina.</string>
<string name="message_reply_to_prefix">En respuesta a</string>
<string name="reply_to_an_image">envió una imagen.</string>
<string name="reply_to_a_video">envió un vídeo.</string>
<string name="reply_to_an_audio_file">envió un archivo de audio.</string>
<string name="reply_to_a_file">envió un archivo.</string>
<!-- Room display name --> <!-- Room display name -->
<string name="room_displayname_invite_from">Invitación de %s</string> <string name="room_displayname_invite_from">Invitación de %s</string>
<string name="room_displayname_room_invite">Invitación a Sala</string> <string name="room_displayname_room_invite">Invitación a Sala</string>

View File

@ -50,8 +50,6 @@
<string name="notice_crypto_unable_to_decrypt">** Ei õnnestu dekrüptida: %s **</string> <string name="notice_crypto_unable_to_decrypt">** Ei õnnestu dekrüptida: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">Sõnumi saatja seade ei ole selle sõnumi jaoks saatnud dekrüptimisvõtmeid.</string> <string name="notice_crypto_error_unkwown_inbound_session_id">Sõnumi saatja seade ei ole selle sõnumi jaoks saatnud dekrüptimisvõtmeid.</string>
<string name="message_reply_to_prefix">Vastuseks kasutajale</string>
<string name="could_not_redact">Ei saanud muuta sõnumit</string> <string name="could_not_redact">Ei saanud muuta sõnumit</string>
<string name="unable_to_send_message">Sõnumi saatmine ei õnnestunud</string> <string name="unable_to_send_message">Sõnumi saatmine ei õnnestunud</string>
@ -67,11 +65,6 @@
<string name="medium_email">E-posti aadress</string> <string name="medium_email">E-posti aadress</string>
<string name="medium_phone_number">Telefoninumber</string> <string name="medium_phone_number">Telefoninumber</string>
<string name="reply_to_an_image">saatis pildi.</string>
<string name="reply_to_a_video">saatis video.</string>
<string name="reply_to_an_audio_file">saatis helifaili.</string>
<string name="reply_to_a_file">saatis faili.</string>
<string name="room_displayname_invite_from">Kutse kasutajalt %s</string> <string name="room_displayname_invite_from">Kutse kasutajalt %s</string>
<string name="room_displayname_room_invite">Kutse jututuppa</string> <string name="room_displayname_room_invite">Kutse jututuppa</string>

View File

@ -63,13 +63,6 @@
<string name="summary_user_sent_sticker">%1$s erabiltzaileak eranskailu bat bidali du.</string> <string name="summary_user_sent_sticker">%1$s erabiltzaileak eranskailu bat bidali du.</string>
<string name="message_reply_to_prefix">Honi erantzunez</string>
<string name="reply_to_an_image">irudi bat bidali du.</string>
<string name="reply_to_a_video">bideo bat bidali du.</string>
<string name="reply_to_an_audio_file">audio fitxategi bat bidali du.</string>
<string name="reply_to_a_file">fitxategi bat bidali du.</string>
<string name="room_displayname_invite_from">%s gelarako gonbidapena</string> <string name="room_displayname_invite_from">%s gelarako gonbidapena</string>
<string name="room_displayname_room_invite">Gela gonbidapena</string> <string name="room_displayname_room_invite">Gela gonbidapena</string>
<string name="room_displayname_two_members">%1$s eta %2$s</string> <string name="room_displayname_two_members">%1$s eta %2$s</string>

View File

@ -51,8 +51,6 @@
<string name="notice_crypto_unable_to_decrypt">** ناتوان در رمزگشایی: %s **</string> <string name="notice_crypto_unable_to_decrypt">** ناتوان در رمزگشایی: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">دستگاه فرستنده، کلیدهای این پیام را برایمان نفرستاده است.</string> <string name="notice_crypto_error_unkwown_inbound_session_id">دستگاه فرستنده، کلیدهای این پیام را برایمان نفرستاده است.</string>
<string name="message_reply_to_prefix">در پاسخ به</string>
<string name="unable_to_send_message">ناتوان در فرستادن پیام</string> <string name="unable_to_send_message">ناتوان در فرستادن پیام</string>
<string name="message_failed_to_upload">شکست در بارگذاری تصویر</string> <string name="message_failed_to_upload">شکست در بارگذاری تصویر</string>
@ -67,11 +65,6 @@
<string name="medium_email">نشانی رایانامه</string> <string name="medium_email">نشانی رایانامه</string>
<string name="medium_phone_number">شماره تلفن</string> <string name="medium_phone_number">شماره تلفن</string>
<string name="reply_to_an_image">تصویری فرستاد.</string>
<string name="reply_to_a_video">ویدیویی فرستاد.</string>
<string name="reply_to_an_audio_file">پرونده‌ای صوتی فرستاد.</string>
<string name="reply_to_a_file">پرونده‌ای فرستاد.</string>
<string name="room_displayname_invite_from">دعوت از %s</string> <string name="room_displayname_invite_from">دعوت از %s</string>
<string name="room_displayname_room_invite">دعوت اتاق</string> <string name="room_displayname_room_invite">دعوت اتاق</string>

View File

@ -70,13 +70,6 @@
<string name="summary_user_sent_sticker">%1$s lähetti tarran.</string> <string name="summary_user_sent_sticker">%1$s lähetti tarran.</string>
<string name="message_reply_to_prefix">Vastauksena käyttäjälle</string>
<string name="reply_to_an_image">oli lähettänyt kuvan.</string>
<string name="reply_to_a_video">lähetti videon.</string>
<string name="reply_to_an_audio_file">lähetti äänitiedoston.</string>
<string name="reply_to_a_file">lähetti tiedoston.</string>
<plurals name="room_displayname_three_and_more_members"> <plurals name="room_displayname_three_and_more_members">
<item quantity="one">%1$s ja yksi muu</item> <item quantity="one">%1$s ja yksi muu</item>
<item quantity="other">%1$s ja %2$d muuta</item> <item quantity="other">%1$s ja %2$d muuta</item>

View File

@ -63,13 +63,6 @@
<string name="summary_user_sent_sticker">%1$s a envoyé un sticker.</string> <string name="summary_user_sent_sticker">%1$s a envoyé un sticker.</string>
<string name="message_reply_to_prefix">En réponse à</string>
<string name="reply_to_an_image">a envoyé une image.</string>
<string name="reply_to_a_video">a envoyé une vidéo.</string>
<string name="reply_to_an_audio_file">a envoyé un fichier audio.</string>
<string name="reply_to_a_file">a envoyé un fichier.</string>
<string name="room_displayname_invite_from">Invitation de %s</string> <string name="room_displayname_invite_from">Invitation de %s</string>
<string name="room_displayname_room_invite">Invitation au salon</string> <string name="room_displayname_room_invite">Invitation au salon</string>
<string name="room_displayname_empty_room">Salon vide</string> <string name="room_displayname_empty_room">Salon vide</string>

View File

@ -50,8 +50,6 @@
<string name="notice_crypto_unable_to_decrypt">** Imposíbel descifrar: %s **</string> <string name="notice_crypto_unable_to_decrypt">** Imposíbel descifrar: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">O dispositivo do que envía non enviou as chaves desta mensaxe.</string> <string name="notice_crypto_error_unkwown_inbound_session_id">O dispositivo do que envía non enviou as chaves desta mensaxe.</string>
<string name="message_reply_to_prefix">Respondéndolle a</string>
<string name="could_not_redact">Non se puido redactar</string> <string name="could_not_redact">Non se puido redactar</string>
<string name="unable_to_send_message">Non foi posíbel enviar a mensaxe</string> <string name="unable_to_send_message">Non foi posíbel enviar a mensaxe</string>
@ -64,11 +62,6 @@
<string name="medium_phone_number">Número de teléfono</string> <string name="medium_phone_number">Número de teléfono</string>
<string name="reply_to_an_image">Responder a</string>
<string name="reply_to_a_video">enviar un vídeo.</string>
<string name="reply_to_an_audio_file">enviar un ficheiro de son.</string>
<string name="reply_to_a_file">enviar un ficheiro.</string>
<string name="room_displayname_two_members">%1$s e %2$s</string> <string name="room_displayname_two_members">%1$s e %2$s</string>

View File

@ -62,13 +62,6 @@
<string name="summary_user_sent_sticker">%1$s küldött egy matricát.</string> <string name="summary_user_sent_sticker">%1$s küldött egy matricát.</string>
<string name="message_reply_to_prefix">Válasz erre:</string>
<string name="reply_to_an_image">képet küldött.</string>
<string name="reply_to_a_video">videót küldött.</string>
<string name="reply_to_an_audio_file">hangfájlt küldött.</string>
<string name="reply_to_a_file">fájlt küldött.</string>
<string name="room_displayname_invite_from">Meghívó tőle: %s</string> <string name="room_displayname_invite_from">Meghívó tőle: %s</string>
<string name="room_displayname_room_invite">Meghívó egy szobába</string> <string name="room_displayname_room_invite">Meghívó egy szobába</string>
<string name="room_displayname_two_members">%1$s és %2$s</string> <string name="room_displayname_two_members">%1$s és %2$s</string>

View File

@ -24,7 +24,6 @@
<string name="notice_avatar_changed_too">(einnig var skipt um auðkennismynd)</string> <string name="notice_avatar_changed_too">(einnig var skipt um auðkennismynd)</string>
<string name="notice_crypto_unable_to_decrypt">** Mistókst að afkóða: %s **</string> <string name="notice_crypto_unable_to_decrypt">** Mistókst að afkóða: %s **</string>
<string name="message_reply_to_prefix">Sem svar til</string>
<string name="unable_to_send_message">Gat ekki sent skilaboð</string> <string name="unable_to_send_message">Gat ekki sent skilaboð</string>

View File

@ -62,13 +62,6 @@
<string name="summary_user_sent_sticker">%1$s ha inviato un adesivo.</string> <string name="summary_user_sent_sticker">%1$s ha inviato un adesivo.</string>
<string name="message_reply_to_prefix">In risposta a</string>
<string name="reply_to_an_image">inviata un\'immagine.</string>
<string name="reply_to_a_video">inviato un video.</string>
<string name="reply_to_an_audio_file">inviato un file audio.</string>
<string name="reply_to_a_file">inviato un file.</string>
<!-- Room display name --> <!-- Room display name -->
<string name="room_displayname_invite_from">Invito da %s</string> <string name="room_displayname_invite_from">Invito da %s</string>
<string name="room_displayname_room_invite">Invito nella stanza</string> <string name="room_displayname_room_invite">Invito nella stanza</string>

View File

@ -56,8 +56,6 @@
<string name="notice_crypto_unable_to_decrypt">** 解読できません: %s **</string> <string name="notice_crypto_unable_to_decrypt">** 解読できません: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">送信者の端末からこのメッセージのキーが送信されていません。</string> <string name="notice_crypto_error_unkwown_inbound_session_id">送信者の端末からこのメッセージのキーが送信されていません。</string>
<string name="message_reply_to_prefix">に返信</string>
<string name="could_not_redact">修正できませんでした</string> <string name="could_not_redact">修正できませんでした</string>
<string name="unable_to_send_message">メッセージを送信できません</string> <string name="unable_to_send_message">メッセージを送信できません</string>
@ -73,9 +71,4 @@
<string name="medium_email">メールアドレス</string> <string name="medium_email">メールアドレス</string>
<string name="medium_phone_number">電話番号</string> <string name="medium_phone_number">電話番号</string>
<string name="reply_to_an_image">画像を送信しました。</string>
<string name="reply_to_a_video">動画を送りました。</string>
<string name="reply_to_an_audio_file">音声ファイルを送信しました。</string>
<string name="reply_to_a_file">ファイルを送信しました。</string>
</resources> </resources>

View File

@ -52,8 +52,6 @@
<string name="notice_crypto_unable_to_decrypt">** 암호를 복호화할 수 없음: %s **</string> <string name="notice_crypto_unable_to_decrypt">** 암호를 복호화할 수 없음: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">발신인의 기기에서 이 메시지의 키를 보내지 않았습니다.</string> <string name="notice_crypto_error_unkwown_inbound_session_id">발신인의 기기에서 이 메시지의 키를 보내지 않았습니다.</string>
<string name="message_reply_to_prefix">관련 대화</string>
<string name="could_not_redact">검열할 수 없습니다</string> <string name="could_not_redact">검열할 수 없습니다</string>
<string name="unable_to_send_message">메시지를 보낼 수 없습니다</string> <string name="unable_to_send_message">메시지를 보낼 수 없습니다</string>
@ -69,11 +67,6 @@
<string name="medium_email">이메일 주소</string> <string name="medium_email">이메일 주소</string>
<string name="medium_phone_number">전화번호</string> <string name="medium_phone_number">전화번호</string>
<string name="reply_to_an_image">사진을 보냈습니다.</string>
<string name="reply_to_a_video">동영상을 보냈습니다.</string>
<string name="reply_to_an_audio_file">오디오 파일을 보냈습니다.</string>
<string name="reply_to_a_file">파일을 보냈습니다.</string>
<string name="room_displayname_invite_from">%s에서 초대함</string> <string name="room_displayname_invite_from">%s에서 초대함</string>
<string name="room_displayname_room_invite">방 초대</string> <string name="room_displayname_room_invite">방 초대</string>

View File

@ -71,13 +71,6 @@
<string name="summary_user_sent_sticker">%1$s heeft een sticker gestuurd.</string> <string name="summary_user_sent_sticker">%1$s heeft een sticker gestuurd.</string>
<string name="message_reply_to_prefix">Als antwoord op</string>
<string name="reply_to_an_image">heeft een afbeelding gestuurd.</string>
<string name="reply_to_a_video">heeft een video gestuurd.</string>
<string name="reply_to_an_audio_file">heeft een audiobestand gestuurd.</string>
<string name="reply_to_a_file">heeft een bestand gestuurd.</string>
<!-- Room display name --> <!-- Room display name -->
<string name="room_displayname_invite_from">Uitnodiging van %s</string> <string name="room_displayname_invite_from">Uitnodiging van %s</string>
<string name="room_displayname_room_invite">Gespreksuitnodiging</string> <string name="room_displayname_room_invite">Gespreksuitnodiging</string>

View File

@ -49,8 +49,6 @@
<string name="notice_crypto_unable_to_decrypt">** Fekk ikkje til å dekryptera: %s **</string> <string name="notice_crypto_unable_to_decrypt">** Fekk ikkje til å dekryptera: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">Avsendareiningi hev ikkje sendt oss nyklane fyr denna meldingi.</string> <string name="notice_crypto_error_unkwown_inbound_session_id">Avsendareiningi hev ikkje sendt oss nyklane fyr denna meldingi.</string>
<string name="message_reply_to_prefix">Som svar til</string>
<string name="could_not_redact">Kunde ikkje gjera um</string> <string name="could_not_redact">Kunde ikkje gjera um</string>
<string name="unable_to_send_message">Fekk ikkje å senda meldingi</string> <string name="unable_to_send_message">Fekk ikkje å senda meldingi</string>
@ -64,11 +62,6 @@
<string name="medium_email">Epostadresse</string> <string name="medium_email">Epostadresse</string>
<string name="medium_phone_number">Telefonnummer</string> <string name="medium_phone_number">Telefonnummer</string>
<string name="reply_to_an_image">sende eit bilæte.</string>
<string name="reply_to_a_video">sende ein video.</string>
<string name="reply_to_an_audio_file">sende ei ljodfil.</string>
<string name="reply_to_a_file">sende ei fil.</string>
<string name="room_displayname_invite_from">Innbjoding frå %s</string> <string name="room_displayname_invite_from">Innbjoding frå %s</string>
<string name="room_displayname_room_invite">Rominnbjoding</string> <string name="room_displayname_room_invite">Rominnbjoding</string>
<string name="room_displayname_two_members">%1$s og %2$s</string> <string name="room_displayname_two_members">%1$s og %2$s</string>

View File

@ -42,7 +42,6 @@
<string name="notice_room_withdraw">%1$s wycofał(a) zaproszenie %2$s</string> <string name="notice_room_withdraw">%1$s wycofał(a) zaproszenie %2$s</string>
<string name="notice_answered_call">%s odebrał(a) połączenie.</string> <string name="notice_answered_call">%s odebrał(a) połączenie.</string>
<string name="notice_avatar_changed_too">(awatar też został zmieniony)</string> <string name="notice_avatar_changed_too">(awatar też został zmieniony)</string>
<string name="message_reply_to_prefix">W odpowiedzi do</string>
<string name="room_displayname_invite_from">Zaproszenie od %s</string> <string name="room_displayname_invite_from">Zaproszenie od %s</string>
<string name="room_displayname_room_invite">Zaproszenie do pokoju</string> <string name="room_displayname_room_invite">Zaproszenie do pokoju</string>
@ -76,11 +75,6 @@
<string name="could_not_redact">Nie można zredagować</string> <string name="could_not_redact">Nie można zredagować</string>
<string name="room_error_join_failed_empty_room">Obecnie nie jest możliwe ponowne dołączenie do pustego pokoju.</string> <string name="room_error_join_failed_empty_room">Obecnie nie jest możliwe ponowne dołączenie do pustego pokoju.</string>
<string name="reply_to_an_image">wyślij zdjęcie.</string>
<string name="reply_to_a_video">wyślij wideo.</string>
<string name="reply_to_an_audio_file">wyślij plik audio.</string>
<string name="reply_to_a_file">wyślij plik.</string>
<string name="notice_event_redacted">Wiadomość usunięta</string> <string name="notice_event_redacted">Wiadomość usunięta</string>
<string name="notice_event_redacted_by">Wiadomość usunięta przez %1$s</string> <string name="notice_event_redacted_by">Wiadomość usunięta przez %1$s</string>
<string name="notice_event_redacted_with_reason">Wiadomość usunięta [powód: %1$s]</string> <string name="notice_event_redacted_with_reason">Wiadomość usunięta [powód: %1$s]</string>

View File

@ -74,13 +74,6 @@
<string name="summary_user_sent_sticker">%1$s enviou um sticker.</string> <string name="summary_user_sent_sticker">%1$s enviou um sticker.</string>
<string name="message_reply_to_prefix">Em resposta a</string>
<string name="reply_to_an_image">enviou uma imagem.</string>
<string name="reply_to_a_video">enviou um vídeo.</string>
<string name="reply_to_an_audio_file">enviou um arquivo de áudio.</string>
<string name="reply_to_a_file">enviou um arquivo.</string>
<!-- Room display name --> <!-- Room display name -->
<string name="room_displayname_invite_from">Convite de %s</string> <string name="room_displayname_invite_from">Convite de %s</string>
<string name="room_displayname_room_invite">Convite para sala</string> <string name="room_displayname_room_invite">Convite para sala</string>

View File

@ -73,13 +73,6 @@
<string name="summary_user_sent_sticker">%1$s отправил стикер.</string> <string name="summary_user_sent_sticker">%1$s отправил стикер.</string>
<string name="message_reply_to_prefix">В ответ на</string>
<string name="reply_to_an_image">отправил изображение.</string>
<string name="reply_to_a_video">отправил видео.</string>
<string name="reply_to_an_audio_file">отправил аудиофайл.</string>
<string name="reply_to_a_file">отправил файл.</string>
<!-- Room display name --> <!-- Room display name -->
<string name="room_displayname_invite_from">Приглашение от %s</string> <string name="room_displayname_invite_from">Приглашение от %s</string>
<string name="room_displayname_room_invite">Приглашение в комнату</string> <string name="room_displayname_room_invite">Приглашение в комнату</string>

View File

@ -62,13 +62,6 @@
<string name="summary_user_sent_sticker">%1$s poslal nálepku.</string> <string name="summary_user_sent_sticker">%1$s poslal nálepku.</string>
<string name="message_reply_to_prefix">Odpoveď na</string>
<string name="reply_to_an_image">odoslal obrázok.</string>
<string name="reply_to_a_video">odoslal video.</string>
<string name="reply_to_an_audio_file">odoslal zvukový súbor.</string>
<string name="reply_to_a_file">Odoslal súbor.</string>
<string name="room_displayname_invite_from">Pozvanie od %s</string> <string name="room_displayname_invite_from">Pozvanie od %s</string>
<string name="room_displayname_room_invite">Pozvanie do miestnosti</string> <string name="room_displayname_room_invite">Pozvanie do miestnosti</string>
<string name="room_displayname_two_members">%1$s a %2$s</string> <string name="room_displayname_two_members">%1$s a %2$s</string>

View File

@ -34,8 +34,6 @@
<string name="notice_crypto_unable_to_decrypt">** Sarrihet të shfshehtëzohet: %s **</string> <string name="notice_crypto_unable_to_decrypt">** Sarrihet të shfshehtëzohet: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">Pajisja e dërguesit nuk na ka dërguar kyçet për këtë mesazh.</string> <string name="notice_crypto_error_unkwown_inbound_session_id">Pajisja e dërguesit nuk na ka dërguar kyçet për këtë mesazh.</string>
<string name="message_reply_to_prefix">Në përgjigje të</string>
<string name="could_not_redact">Su redaktua dot</string> <string name="could_not_redact">Su redaktua dot</string>
<string name="unable_to_send_message">Sarrihet të dërgohet mesazh</string> <string name="unable_to_send_message">Sarrihet të dërgohet mesazh</string>
@ -51,11 +49,6 @@
<string name="medium_email">Adresë email</string> <string name="medium_email">Adresë email</string>
<string name="medium_phone_number">Numër telefoni</string> <string name="medium_phone_number">Numër telefoni</string>
<string name="reply_to_an_image">dërgoi një figurë.</string>
<string name="reply_to_a_video">dërgoi një video.</string>
<string name="reply_to_an_audio_file">dërgoi një kartelë audio.</string>
<string name="reply_to_a_file">dërgoi një kartelë.</string>
<string name="room_displayname_invite_from">Ftesë nga %s</string> <string name="room_displayname_invite_from">Ftesë nga %s</string>
<string name="room_displayname_room_invite">Ftesë Dhome</string> <string name="room_displayname_room_invite">Ftesë Dhome</string>

View File

@ -56,8 +56,6 @@
<string name="notice_crypto_unable_to_decrypt">** Неможливо розшифрувати: %s **</string> <string name="notice_crypto_unable_to_decrypt">** Неможливо розшифрувати: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">Пристрій відправника не надіслав нам ключ для цього повідомлення.</string> <string name="notice_crypto_error_unkwown_inbound_session_id">Пристрій відправника не надіслав нам ключ для цього повідомлення.</string>
<string name="message_reply_to_prefix">У відповідь на</string>
<string name="could_not_redact">Неможливо відредагувати</string> <string name="could_not_redact">Неможливо відредагувати</string>
<string name="unable_to_send_message">Не вдалося надіслати повідомлення</string> <string name="unable_to_send_message">Не вдалося надіслати повідомлення</string>
@ -71,11 +69,6 @@
<string name="medium_email">Адреса електронної пошти</string> <string name="medium_email">Адреса електронної пошти</string>
<string name="medium_phone_number">Номер телефону</string> <string name="medium_phone_number">Номер телефону</string>
<string name="reply_to_an_image">надіслав зображення.</string>
<string name="reply_to_a_video">надіслав відео.</string>
<string name="reply_to_an_audio_file">надіслав аудіо файл.</string>
<string name="reply_to_a_file">надіслав файл.</string>
<plurals name="room_displayname_three_and_more_members"> <plurals name="room_displayname_three_and_more_members">
<item quantity="one">%1$s та 1 інший</item> <item quantity="one">%1$s та 1 інший</item>
<item quantity="few">%1$s та %2$d інші</item> <item quantity="few">%1$s та %2$d інші</item>

View File

@ -50,8 +50,6 @@
<string name="notice_crypto_unable_to_decrypt">** Kun nie ountsleuteln: %s **</string> <string name="notice_crypto_unable_to_decrypt">** Kun nie ountsleuteln: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">t Toestel van den afzender èt geen sleutels vo da bericht hier gesteurd.</string> <string name="notice_crypto_error_unkwown_inbound_session_id">t Toestel van den afzender èt geen sleutels vo da bericht hier gesteurd.</string>
<string name="message_reply_to_prefix">Als antwoord ip</string>
<string name="could_not_redact">Kosteg nie verwyderd wordn</string> <string name="could_not_redact">Kosteg nie verwyderd wordn</string>
<string name="unable_to_send_message">Kosteg t bericht nie verzendn</string> <string name="unable_to_send_message">Kosteg t bericht nie verzendn</string>
@ -67,11 +65,6 @@
<string name="medium_email">E-mailadresse</string> <string name="medium_email">E-mailadresse</string>
<string name="medium_phone_number">Telefongnumero</string> <string name="medium_phone_number">Telefongnumero</string>
<string name="reply_to_an_image">èt e fotootje gesteurd.</string>
<string name="reply_to_a_video">èt e filmtje gesteurd.</string>
<string name="reply_to_an_audio_file">èt e geluudsfragment gesteurd.</string>
<string name="reply_to_a_file">èt e bestand gesteurd.</string>
<string name="room_displayname_invite_from">Uutnodigienge van %s</string> <string name="room_displayname_invite_from">Uutnodigienge van %s</string>
<string name="room_displayname_room_invite">Gespreksuutnodigienge</string> <string name="room_displayname_room_invite">Gespreksuutnodigienge</string>

View File

@ -63,13 +63,6 @@
<string name="summary_message">%1$s%2$s</string> <string name="summary_message">%1$s%2$s</string>
<string name="summary_user_sent_sticker">%1$s 发送了一张贴纸。</string> <string name="summary_user_sent_sticker">%1$s 发送了一张贴纸。</string>
<string name="reply_to_an_image">发送了一张图片。</string>
<string name="reply_to_a_video">发送了一个视频。</string>
<string name="reply_to_an_audio_file">发送了一段音频。</string>
<string name="reply_to_a_file">发送了一个文件。</string>
<string name="message_reply_to_prefix">回复</string>
<string name="room_displayname_empty_room">空聊天室</string> <string name="room_displayname_empty_room">空聊天室</string>
<string name="room_displayname_invite_from">来自 %s 的邀请</string> <string name="room_displayname_invite_from">来自 %s 的邀请</string>
<string name="room_displayname_room_invite">聊天室邀请</string> <string name="room_displayname_room_invite">聊天室邀请</string>

View File

@ -62,13 +62,6 @@
<string name="summary_user_sent_sticker">%1$s 傳送了一張貼圖。</string> <string name="summary_user_sent_sticker">%1$s 傳送了一張貼圖。</string>
<string name="message_reply_to_prefix">回覆</string>
<string name="reply_to_an_image">傳送了圖片。</string>
<string name="reply_to_a_video">傳送了影片。</string>
<string name="reply_to_an_audio_file">傳送了音訊檔案。</string>
<string name="reply_to_a_file">傳送了檔案。</string>
<string name="room_displayname_invite_from">來自%s 的邀請</string> <string name="room_displayname_invite_from">來自%s 的邀請</string>
<string name="room_displayname_room_invite">聊天室邀請</string> <string name="room_displayname_room_invite">聊天室邀請</string>
<string name="room_displayname_two_members">%1$s 和 %2$s</string> <string name="room_displayname_two_members">%1$s 和 %2$s</string>

View File

@ -112,7 +112,6 @@
<string name="notice_crypto_error_unkwown_inbound_session_id">The sender\'s device has not sent us the keys for this message.</string> <string name="notice_crypto_error_unkwown_inbound_session_id">The sender\'s device has not sent us the keys for this message.</string>
<!-- Messages --> <!-- Messages -->
<string name="message_reply_to_prefix">In reply to</string>
<!-- Room Screen --> <!-- Room Screen -->
<string name="could_not_redact">Could not redact</string> <string name="could_not_redact">Could not redact</string>
@ -139,12 +138,6 @@
<string name="medium_email">Email address</string> <string name="medium_email">Email address</string>
<string name="medium_phone_number">Phone number</string> <string name="medium_phone_number">Phone number</string>
<!-- Reply to -->
<string name="reply_to_an_image">sent an image.</string>
<string name="reply_to_a_video">sent a video.</string>
<string name="reply_to_an_audio_file">sent an audio file.</string>
<string name="reply_to_a_file">sent a file.</string>
<!-- Room display name --> <!-- Room display name -->
<string name="room_displayname_invite_from">Invite from %s</string> <string name="room_displayname_invite_from">Invite from %s</string>
<string name="room_displayname_room_invite">Room Invite</string> <string name="room_displayname_room_invite">Room Invite</string>

View File

@ -17,7 +17,7 @@ androidExtensions {
// Note: 2 digits max for each value // Note: 2 digits max for each value
ext.versionMajor = 0 ext.versionMajor = 0
ext.versionMinor = 91 ext.versionMinor = 91
ext.versionPatch = 3 ext.versionPatch = 4
static def getGitTimestamp() { static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct' def cmd = 'git show -s --format=%ct'
@ -106,6 +106,11 @@ def buildNumber = System.env.BUILDKITE_BUILD_NUMBER as Integer ?: 0
android { android {
compileSdkVersion 29 compileSdkVersion 29
// Due to a bug introduced in Android gradle plugin 3.6.0, we have to specify the ndk version to use
// Ref: https://issuetracker.google.com/issues/144111441
ndkVersion "21.3.6528147"
defaultConfig { defaultConfig {
applicationId "im.vector.app" applicationId "im.vector.app"
// Set to API 21: see #405 // Set to API 21: see #405
@ -232,8 +237,7 @@ android {
lintOptions { lintOptions {
lintConfig file("lint.xml") lintConfig file("lint.xml")
// TODO Restore true once pb with WorkManager is fixed abortOnError true
abortOnError false
} }
compileOptions { compileOptions {

View File

@ -21,3 +21,6 @@
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
-keep class im.vector.riotx.features.** { *; } -keep class im.vector.riotx.features.** { *; }
## print all the rules in a file
# -printconfiguration ../proguard_files/full-r8-config.txt

View File

@ -247,6 +247,12 @@
<!-- Providers --> <!-- Providers -->
<!-- Remove WorkManagerInitializer Provider because we are using on-demand initialization of WorkManager-->
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileProvider" android:authorities="${applicationId}.fileProvider"

View File

@ -63,7 +63,13 @@ import java.util.Locale
import java.util.concurrent.Executors import java.util.concurrent.Executors
import javax.inject.Inject import javax.inject.Inject
class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.Provider, androidx.work.Configuration.Provider { import androidx.work.Configuration as WorkConfiguration
class VectorApplication :
Application(),
HasVectorInjector,
MatrixConfiguration.Provider,
WorkConfiguration.Provider {
lateinit var appContext: Context lateinit var appContext: Context
@Inject lateinit var legacySessionImporter: LegacySessionImporter @Inject lateinit var legacySessionImporter: LegacySessionImporter
@ -85,6 +91,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
@Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager @Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
lateinit var vectorComponent: VectorComponent lateinit var vectorComponent: VectorComponent
// font thread handler // font thread handler
private var fontThreadHandler: Handler? = null private var fontThreadHandler: Handler? = null
@ -157,7 +164,11 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
override fun providesMatrixConfiguration() = MatrixConfiguration(BuildConfig.FLAVOR_DESCRIPTION) override fun providesMatrixConfiguration() = MatrixConfiguration(BuildConfig.FLAVOR_DESCRIPTION)
override fun getWorkManagerConfiguration() = androidx.work.Configuration.Builder().setExecutor(Executors.newCachedThreadPool()).build() override fun getWorkManagerConfiguration(): WorkConfiguration {
return WorkConfiguration.Builder()
.setExecutor(Executors.newCachedThreadPool())
.build()
}
override fun injector(): VectorComponent { override fun injector(): VectorComponent {
return vectorComponent return vectorComponent

View File

@ -17,29 +17,39 @@
package im.vector.riotx.core.utils package im.vector.riotx.core.utils
import android.app.Activity import android.app.Activity
import android.app.DownloadManager
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.media.MediaScannerConnection
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment
import android.provider.Browser import android.provider.Browser
import android.provider.MediaStore import android.provider.MediaStore
import android.webkit.MimeTypeMap
import android.widget.Toast import android.widget.Toast
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.browser.customtabs.CustomTabsSession import androidx.browser.customtabs.CustomTabsSession
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.extensions.tryThis
import im.vector.riotx.BuildConfig import im.vector.riotx.BuildConfig
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.features.notifications.NotificationUtils import im.vector.riotx.features.notifications.NotificationUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import okio.buffer import okio.buffer
import okio.sink import okio.sink
import okio.source import okio.source
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@ -301,42 +311,20 @@ fun shareMedia(context: Context, file: File, mediaMimeType: String?) {
fun saveMedia(context: Context, file: File, title: String, mediaMimeType: String?, notificationUtils: NotificationUtils) { fun saveMedia(context: Context, file: File, title: String, mediaMimeType: String?, notificationUtils: NotificationUtils) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val externalContentUri: Uri val values = ContentValues().apply {
val values = ContentValues() put(MediaStore.Images.Media.TITLE, title)
when { put(MediaStore.Images.Media.DISPLAY_NAME, title)
mediaMimeType?.startsWith("image/") == true -> { put(MediaStore.Images.Media.MIME_TYPE, mediaMimeType)
externalContentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis())
values.put(MediaStore.Images.Media.TITLE, title) put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
values.put(MediaStore.Images.Media.DISPLAY_NAME, title)
values.put(MediaStore.Images.Media.MIME_TYPE, mediaMimeType)
values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis())
values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
}
mediaMimeType?.startsWith("video/") == true -> {
externalContentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
values.put(MediaStore.Video.Media.TITLE, title)
values.put(MediaStore.Video.Media.DISPLAY_NAME, title)
values.put(MediaStore.Video.Media.MIME_TYPE, mediaMimeType)
values.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis())
values.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis())
}
mediaMimeType?.startsWith("audio/") == true -> {
externalContentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
values.put(MediaStore.Audio.Media.TITLE, title)
values.put(MediaStore.Audio.Media.DISPLAY_NAME, title)
values.put(MediaStore.Audio.Media.MIME_TYPE, mediaMimeType)
values.put(MediaStore.Audio.Media.DATE_ADDED, System.currentTimeMillis())
values.put(MediaStore.Audio.Media.DATE_TAKEN, System.currentTimeMillis())
}
else -> {
externalContentUri = MediaStore.Downloads.EXTERNAL_CONTENT_URI
values.put(MediaStore.Downloads.TITLE, title)
values.put(MediaStore.Downloads.DISPLAY_NAME, title)
values.put(MediaStore.Downloads.MIME_TYPE, mediaMimeType)
values.put(MediaStore.Downloads.DATE_ADDED, System.currentTimeMillis())
values.put(MediaStore.Downloads.DATE_TAKEN, System.currentTimeMillis())
}
} }
val externalContentUri = when {
mediaMimeType?.startsWith("image/") == true -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
mediaMimeType?.startsWith("video/") == true -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
mediaMimeType?.startsWith("audio/") == true -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
else -> MediaStore.Downloads.EXTERNAL_CONTENT_URI
}
val uri = context.contentResolver.insert(externalContentUri, values) val uri = context.contentResolver.insert(externalContentUri, values)
if (uri == null) { if (uri == null) {
Toast.makeText(context, R.string.error_saving_media_file, Toast.LENGTH_LONG).show() Toast.makeText(context, R.string.error_saving_media_file, Toast.LENGTH_LONG).show()
@ -357,16 +345,70 @@ fun saveMedia(context: Context, file: File, title: String, mediaMimeType: String
notificationUtils.showNotificationMessage("DL", uri.hashCode(), notification) notificationUtils.showNotificationMessage("DL", uri.hashCode(), notification)
} }
} }
// TODO add notification?
} else { } else {
@Suppress("DEPRECATION") saveMediaLegacy(context, mediaMimeType, title, file)
Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE).also { mediaScanIntent -> }
mediaScanIntent.data = Uri.fromFile(file) }
context.sendBroadcast(mediaScanIntent)
@Suppress("DEPRECATION")
private fun saveMediaLegacy(context: Context, mediaMimeType: String?, title: String, file: File) {
val state = Environment.getExternalStorageState()
if (Environment.MEDIA_MOUNTED != state) {
context.toast(context.getString(R.string.error_saving_media_file))
return
}
GlobalScope.launch(Dispatchers.IO) {
val dest = when {
mediaMimeType?.startsWith("image/") == true -> Environment.DIRECTORY_PICTURES
mediaMimeType?.startsWith("video/") == true -> Environment.DIRECTORY_MOVIES
mediaMimeType?.startsWith("audio/") == true -> Environment.DIRECTORY_MUSIC
else -> Environment.DIRECTORY_DOWNLOADS
}
val downloadDir = Environment.getExternalStoragePublicDirectory(dest)
try {
val outputFilename = if (title.substringAfterLast('.', "").isEmpty()) {
val extension = mediaMimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) }
"$title.$extension"
} else {
title
}
val savedFile = saveFileIntoLegacy(file, downloadDir, outputFilename)
if (savedFile != null) {
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as? DownloadManager
downloadManager?.addCompletedDownload(
savedFile.name,
title,
true,
mediaMimeType ?: "application/octet-stream",
savedFile.absolutePath,
savedFile.length(),
true)
addToGallery(savedFile, mediaMimeType, context)
}
} catch (error: Throwable) {
GlobalScope.launch(Dispatchers.Main) {
context.toast(context.getString(R.string.error_saving_media_file))
}
} }
} }
} }
private fun addToGallery(savedFile: File, mediaMimeType: String?, context: Context) {
// MediaScannerConnection provides a way for applications to pass a newly created or downloaded media file to the media scanner service.
var mediaConnection: MediaScannerConnection? = null
val mediaScannerConnectionClient: MediaScannerConnection.MediaScannerConnectionClient = object : MediaScannerConnection.MediaScannerConnectionClient {
override fun onMediaScannerConnected() {
mediaConnection?.scanFile(savedFile.path, mediaMimeType)
}
override fun onScanCompleted(path: String, uri: Uri?) {
if (path == savedFile.path) mediaConnection?.disconnect()
}
}
mediaConnection = MediaScannerConnection(context, mediaScannerConnectionClient).apply { connect() }
}
/** /**
* Open the play store to the provided application Id, default to this app * Open the play store to the provided application Id, default to this app
*/ */
@ -381,3 +423,76 @@ fun openPlayStore(activity: Activity, appId: String = BuildConfig.APPLICATION_ID
} }
} }
} }
// ==============================================================================================================
// Media utils
// ==============================================================================================================
/**
* Copy a file into a dstPath directory.
* The output filename can be provided.
* The output file is not overridden if it is already exist.
*
* ~~ This is copied from the old matrix sdk ~~
*
* @param sourceFile the file source path
* @param dstDirPath the dst path
* @param outputFilename optional the output filename
* @param callback the asynchronous callback
*/
@Suppress("DEPRECATION")
fun saveFileIntoLegacy(sourceFile: File, dstDirPath: File, outputFilename: String?): File? {
// defines another name for the external media
val dstFileName: String
// build a filename is not provided
if (null == outputFilename) {
// extract the file extension from the uri
val dotPos = sourceFile.name.lastIndexOf(".")
var fileExt = ""
if (dotPos > 0) {
fileExt = sourceFile.name.substring(dotPos)
}
dstFileName = "vector_" + System.currentTimeMillis() + fileExt
} else {
dstFileName = outputFilename
}
var dstFile = File(dstDirPath, dstFileName)
// if the file already exists, append a marker
if (dstFile.exists()) {
var baseFileName = dstFileName
var fileExt = ""
val lastDotPos = dstFileName.lastIndexOf(".")
if (lastDotPos > 0) {
baseFileName = dstFileName.substring(0, lastDotPos)
fileExt = dstFileName.substring(lastDotPos)
}
var counter = 1
while (dstFile.exists()) {
dstFile = File(dstDirPath, "$baseFileName($counter)$fileExt")
counter++
}
}
// Copy source file to destination
var inputStream: FileInputStream? = null
var outputStream: FileOutputStream? = null
try {
dstFile.createNewFile()
inputStream = FileInputStream(sourceFile)
outputStream = FileOutputStream(dstFile)
val buffer = ByteArray(1024 * 10)
var len: Int
while (inputStream.read(buffer).also { len = it } != -1) {
outputStream.write(buffer, 0, len)
}
return dstFile
} catch (failure: Throwable) {
return null
} finally {
// Close resources
tryThis { inputStream?.close() }
tryThis { outputStream?.close() }
}
}

View File

@ -37,3 +37,11 @@ internal fun String.ensureProtocol(): String {
else -> this else -> this
} }
} }
internal fun String.ensureTrailingSlash(): String {
return when {
isEmpty() -> this
!endsWith("/") -> "$this/"
else -> this
}
}

View File

@ -56,6 +56,7 @@ abstract class FormEditTextWithButtonItem : VectorEpoxyModel<FormEditTextWithBut
} }
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder)
holder.textInputLayout.isEnabled = enabled holder.textInputLayout.isEnabled = enabled
holder.textInputLayout.hint = hint holder.textInputLayout.hint = hint

View File

@ -22,6 +22,7 @@ import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.Typeface import android.graphics.Typeface
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.text.Spannable import android.text.Spannable
@ -222,6 +223,7 @@ class RoomDetailFragment @Inject constructor(
private const val AUDIO_CALL_PERMISSION_REQUEST_CODE = 1 private const val AUDIO_CALL_PERMISSION_REQUEST_CODE = 1
private const val VIDEO_CALL_PERMISSION_REQUEST_CODE = 2 private const val VIDEO_CALL_PERMISSION_REQUEST_CODE = 2
private const val SAVE_ATTACHEMENT_REQUEST_CODE = 3
/** /**
* Sanitize the display name. * Sanitize the display name.
@ -1194,17 +1196,12 @@ class RoomDetailFragment @Inject constructor(
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (allGranted(grantResults)) { if (allGranted(grantResults)) {
when (requestCode) { when (requestCode) {
// PERMISSION_REQUEST_CODE_DOWNLOAD_FILE -> { SAVE_ATTACHEMENT_REQUEST_CODE -> {
// val action = roomDetailViewModel.pendingAction sharedActionViewModel.pendingAction?.let {
// if (action != null) { handleActions(it)
// (action as? RoomDetailAction.DownloadFile) sharedActionViewModel.pendingAction = null
// ?.messageFileContent }
// ?.getFileName() }
// ?.let { showSnackWithMessage(getString(R.string.downloading_file, it)) }
// roomDetailViewModel.pendingAction = null
// roomDetailViewModel.handle(action)
// }
// }
PERMISSION_REQUEST_CODE_INCOMING_URI -> { PERMISSION_REQUEST_CODE_INCOMING_URI -> {
val pendingUri = roomDetailViewModel.pendingUri val pendingUri = roomDetailViewModel.pendingUri
if (pendingUri != null) { if (pendingUri != null) {
@ -1357,6 +1354,11 @@ class RoomDetailFragment @Inject constructor(
} }
private fun onSaveActionClicked(action: EventSharedAction.Save) { private fun onSaveActionClicked(action: EventSharedAction.Save) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
&& !checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, SAVE_ATTACHEMENT_REQUEST_CODE)) {
sharedActionViewModel.pendingAction = action
return
}
session.fileService().downloadFile( session.fileService().downloadFile(
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
id = action.eventId, id = action.eventId,

View File

@ -21,4 +21,6 @@ import javax.inject.Inject
/** /**
* Activity shared view model to handle message actions * Activity shared view model to handle message actions
*/ */
class MessageSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<EventSharedAction>() class MessageSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<EventSharedAction>() {
var pendingAction : EventSharedAction? = null
}

View File

@ -73,6 +73,9 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
override fun showFailure(throwable: Throwable) { override fun showFailure(throwable: Throwable) {
when (throwable) { when (throwable) {
is Failure.Cancelled ->
/* Ignore this error, user has cancelled the action */
Unit
is Failure.ServerError -> is Failure.ServerError ->
if (throwable.error.code == MatrixError.M_FORBIDDEN if (throwable.error.code == MatrixError.M_FORBIDDEN
&& throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) { && throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) {

View File

@ -151,8 +151,8 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
// TODO Disabled because it provokes a flickering // TODO Disabled because it provokes a flickering
// ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
}) })
is LoginViewEvents.OnServerSelectionDone -> onServerSelectionDone() is LoginViewEvents.OnServerSelectionDone -> onServerSelectionDone(loginViewEvents)
is LoginViewEvents.OnSignModeSelected -> onSignModeSelected() is LoginViewEvents.OnSignModeSelected -> onSignModeSelected(loginViewEvents)
is LoginViewEvents.OnLoginFlowRetrieved -> is LoginViewEvents.OnLoginFlowRetrieved ->
addFragmentToBackstack(R.id.loginFragmentContainer, addFragmentToBackstack(R.id.loginFragmentContainer,
if (loginViewEvents.isSso) { if (loginViewEvents.isSso) {
@ -228,18 +228,20 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
.show() .show()
} }
private fun onServerSelectionDone() = withState(loginViewModel) { state -> private fun onServerSelectionDone(loginViewEvents: LoginViewEvents.OnServerSelectionDone) {
when (state.serverType) { when (loginViewEvents.serverType) {
ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow
ServerType.Modular, ServerType.Modular,
ServerType.Other -> addFragmentToBackstack(R.id.loginFragmentContainer, ServerType.Other -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginServerUrlFormFragment::class.java, LoginServerUrlFormFragment::class.java,
option = commonOption) option = commonOption)
ServerType.Unknown -> Unit /* Should not happen */
} }
} }
private fun onSignModeSelected() = withState(loginViewModel) { state -> private fun onSignModeSelected(loginViewEvents: LoginViewEvents.OnSignModeSelected) = withState(loginViewModel) { state ->
when (state.signMode) { // state.signMode could not be ready yet. So use value from the ViewEvent
when (loginViewEvents.signMode) {
SignMode.Unknown -> error("Sign mode has to be set before calling this method") SignMode.Unknown -> error("Sign mode has to be set before calling this method")
SignMode.SignUp -> { SignMode.SignUp -> {
// This is managed by the LoginViewEvents // This is managed by the LoginViewEvents

View File

@ -54,6 +54,7 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
private var passwordShown = false private var passwordShown = false
private var isSignupMode = false private var isSignupMode = false
// Temporary patch for https://github.com/vector-im/riotX-android/issues/1410, // Temporary patch for https://github.com/vector-im/riotX-android/issues/1410,
// waiting for https://github.com/matrix-org/synapse/issues/7576 // waiting for https://github.com/matrix-org/synapse/issues/7576
private var isNumericOnlyUserIdForbidden = false private var isNumericOnlyUserIdForbidden = false
@ -138,6 +139,7 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
loginServerIcon.isVisible = false loginServerIcon.isVisible = false
loginTitle.text = getString(R.string.login_signin_matrix_id_title) loginTitle.text = getString(R.string.login_signin_matrix_id_title)
loginNotice.text = getString(R.string.login_signin_matrix_id_notice) loginNotice.text = getString(R.string.login_signin_matrix_id_notice)
loginPasswordNotice.isVisible = true
} else { } else {
val resId = when (state.signMode) { val resId = when (state.signMode) {
SignMode.Unknown -> error("developer error") SignMode.Unknown -> error("developer error")
@ -164,7 +166,9 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl())
loginNotice.text = getString(R.string.login_server_other_text) loginNotice.text = getString(R.string.login_server_other_text)
} }
ServerType.Unknown -> Unit /* Should not happen */
} }
loginPasswordNotice.isVisible = false
} }
} }

View File

@ -19,7 +19,6 @@ package im.vector.riotx.features.login
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import butterknife.OnClick import butterknife.OnClick
import com.airbnb.mvrx.withState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.utils.openUrlInChromeCustomTab import im.vector.riotx.core.utils.openUrlInChromeCustomTab
import kotlinx.android.synthetic.main.fragment_login_server_selection.* import kotlinx.android.synthetic.main.fragment_login_server_selection.*
@ -40,11 +39,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment
} }
private fun updateSelectedChoice(state: LoginViewState) { private fun updateSelectedChoice(state: LoginViewState) {
state.serverType.let { loginServerChoiceMatrixOrg.isChecked = state.serverType == ServerType.MatrixOrg
loginServerChoiceMatrixOrg.isChecked = it == ServerType.MatrixOrg
loginServerChoiceModular.isChecked = it == ServerType.Modular
loginServerChoiceOther.isChecked = it == ServerType.Other
}
} }
private fun initTextViews() { private fun initTextViews() {
@ -61,42 +56,17 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment
@OnClick(R.id.loginServerChoiceMatrixOrg) @OnClick(R.id.loginServerChoiceMatrixOrg)
fun selectMatrixOrg() { fun selectMatrixOrg() {
if (loginServerChoiceMatrixOrg.isChecked) { loginViewModel.handle(LoginAction.UpdateServerType(ServerType.MatrixOrg))
// Consider this is a submit
submit()
} else {
loginViewModel.handle(LoginAction.UpdateServerType(ServerType.MatrixOrg))
}
} }
@OnClick(R.id.loginServerChoiceModular) @OnClick(R.id.loginServerChoiceModular)
fun selectModular() { fun selectModular() {
if (loginServerChoiceModular.isChecked) { loginViewModel.handle(LoginAction.UpdateServerType(ServerType.Modular))
// Consider this is a submit
submit()
} else {
loginViewModel.handle(LoginAction.UpdateServerType(ServerType.Modular))
}
} }
@OnClick(R.id.loginServerChoiceOther) @OnClick(R.id.loginServerChoiceOther)
fun selectOther() { fun selectOther() {
if (loginServerChoiceOther.isChecked) { loginViewModel.handle(LoginAction.UpdateServerType(ServerType.Other))
// Consider this is a submit
submit()
} else {
loginViewModel.handle(LoginAction.UpdateServerType(ServerType.Other))
}
}
@OnClick(R.id.loginServerSubmit)
fun submit() = withState(loginViewModel) { state ->
if (state.serverType == ServerType.MatrixOrg) {
// Request login flow here
loginViewModel.handle(LoginAction.UpdateHomeServer(getString(R.string.matrix_org_server_url)))
} else {
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnServerSelectionDone))
}
} }
@OnClick(R.id.loginServerIKnowMyIdSubmit) @OnClick(R.id.loginServerIKnowMyIdSubmit)

View File

@ -70,7 +70,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment()
loginServerUrlFormHomeServerUrlTil.hint = getText(R.string.login_server_url_form_modular_hint) loginServerUrlFormHomeServerUrlTil.hint = getText(R.string.login_server_url_form_modular_hint)
loginServerUrlFormNotice.text = getString(R.string.login_server_url_form_modular_notice) loginServerUrlFormNotice.text = getString(R.string.login_server_url_form_modular_notice)
} }
ServerType.Other -> { else -> {
loginServerUrlFormIcon.isVisible = false loginServerUrlFormIcon.isVisible = false
loginServerUrlFormTitle.text = getString(R.string.login_server_other_title) loginServerUrlFormTitle.text = getString(R.string.login_server_other_title)
loginServerUrlFormText.text = getString(R.string.login_connect_to_a_custom_server) loginServerUrlFormText.text = getString(R.string.login_connect_to_a_custom_server)
@ -78,7 +78,6 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment()
loginServerUrlFormHomeServerUrlTil.hint = getText(R.string.login_server_url_form_other_hint) loginServerUrlFormHomeServerUrlTil.hint = getText(R.string.login_server_url_form_other_hint)
loginServerUrlFormNotice.text = getString(R.string.login_server_url_form_other_notice) loginServerUrlFormNotice.text = getString(R.string.login_server_url_form_other_notice)
} }
else -> error("This fragment should not be displayed in matrix.org mode")
} }
} }

View File

@ -49,6 +49,7 @@ open class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLo
loginSignupSigninTitle.text = getString(R.string.login_server_other_title) loginSignupSigninTitle.text = getString(R.string.login_server_other_title)
loginSignupSigninText.text = getString(R.string.login_connect_to, state.homeServerUrl.toReducedUrl()) loginSignupSigninText.text = getString(R.string.login_connect_to, state.homeServerUrl.toReducedUrl())
} }
ServerType.Unknown -> Unit /* Should not happen */
} }
} }

View File

@ -33,9 +33,9 @@ sealed class LoginViewEvents : VectorViewEvents {
// Navigation event // Navigation event
object OpenServerSelection : LoginViewEvents() object OpenServerSelection : LoginViewEvents()
object OnServerSelectionDone : LoginViewEvents() data class OnServerSelectionDone(val serverType: ServerType) : LoginViewEvents()
data class OnLoginFlowRetrieved(val isSso: Boolean) : LoginViewEvents() data class OnLoginFlowRetrieved(val isSso: Boolean) : LoginViewEvents()
object OnSignModeSelected : LoginViewEvents() data class OnSignModeSelected(val signMode: SignMode) : LoginViewEvents()
object OnForgetPasswordClicked : LoginViewEvents() object OnForgetPasswordClicked : LoginViewEvents()
object OnResetPasswordSendThreePidDone : LoginViewEvents() object OnResetPasswordSendThreePidDone : LoginViewEvents()
object OnResetPasswordMailConfirmationSuccess : LoginViewEvents() object OnResetPasswordMailConfirmationSuccess : LoginViewEvents()

View File

@ -39,6 +39,7 @@ import im.vector.matrix.android.api.auth.registration.RegistrationResult
import im.vector.matrix.android.api.auth.registration.RegistrationWizard import im.vector.matrix.android.api.auth.registration.RegistrationWizard
import im.vector.matrix.android.api.auth.registration.Stage import im.vector.matrix.android.api.auth.registration.Stage
import im.vector.matrix.android.api.auth.wellknown.WellknownResult import im.vector.matrix.android.api.auth.wellknown.WellknownResult
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.riotx.R import im.vector.riotx.R
@ -47,6 +48,7 @@ import im.vector.riotx.core.extensions.configureAndStart
import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.ensureTrailingSlash
import im.vector.riotx.features.call.WebRtcPeerConnectionManager import im.vector.riotx.features.call.WebRtcPeerConnectionManager
import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.session.SessionListener import im.vector.riotx.features.session.SessionListener
@ -87,8 +89,12 @@ class LoginViewModel @AssistedInject constructor(
} }
} }
// Store the last action, to redo it after user has trusted the untrusted certificate
private var lastAction: LoginAction? = null
private var currentHomeServerConnectionConfig: HomeServerConnectionConfig? = null private var currentHomeServerConnectionConfig: HomeServerConnectionConfig? = null
private val matrixOrgUrl = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash()
val currentThreePid: String? val currentThreePid: String?
get() = registrationWizard?.currentThreePid get() = registrationWizard?.currentThreePid
@ -111,8 +117,8 @@ class LoginViewModel @AssistedInject constructor(
is LoginAction.UpdateServerType -> handleUpdateServerType(action) is LoginAction.UpdateServerType -> handleUpdateServerType(action)
is LoginAction.UpdateSignMode -> handleUpdateSignMode(action) is LoginAction.UpdateSignMode -> handleUpdateSignMode(action)
is LoginAction.InitWith -> handleInitWith(action) is LoginAction.InitWith -> handleInitWith(action)
is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action) is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action).also { lastAction = action }
is LoginAction.LoginOrRegister -> handleLoginOrRegister(action) is LoginAction.LoginOrRegister -> handleLoginOrRegister(action).also { lastAction = action }
is LoginAction.LoginWithToken -> handleLoginWithToken(action) is LoginAction.LoginWithToken -> handleLoginWithToken(action)
is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action) is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action)
is LoginAction.ResetPassword -> handleResetPassword(action) is LoginAction.ResetPassword -> handleResetPassword(action)
@ -126,10 +132,23 @@ class LoginViewModel @AssistedInject constructor(
} }
private fun handleUserAcceptCertificate(action: LoginAction.UserAcceptCertificate) { private fun handleUserAcceptCertificate(action: LoginAction.UserAcceptCertificate) {
// It happen when we get the login flow, so alter the homeserver config and retrieve again the login flow // It happen when we get the login flow, or during direct authentication.
currentHomeServerConnectionConfig // So alter the homeserver config and retrieve again the login flow
?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) } when (val finalLastAction = lastAction) {
?.let { getLoginFlow(it) } is LoginAction.UpdateHomeServer ->
currentHomeServerConnectionConfig
?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) }
?.let { getLoginFlow(it) }
is LoginAction.LoginOrRegister ->
handleDirectLogin(
finalLastAction,
HomeServerConnectionConfig.Builder()
// Will be replaced by the task
.withHomeServerUri("https://dummy.org")
.withAllowedFingerPrints(listOf(action.fingerprint))
.build()
)
}
} }
private fun handleLoginWithToken(action: LoginAction.LoginWithToken) { private fun handleLoginWithToken(action: LoginAction.LoginWithToken) {
@ -321,7 +340,7 @@ class LoginViewModel @AssistedInject constructor(
LoginAction.ResetHomeServerType -> { LoginAction.ResetHomeServerType -> {
setState { setState {
copy( copy(
serverType = ServerType.MatrixOrg serverType = ServerType.Unknown
) )
} }
} }
@ -333,6 +352,7 @@ class LoginViewModel @AssistedInject constructor(
asyncHomeServerLoginFlowRequest = Uninitialized, asyncHomeServerLoginFlowRequest = Uninitialized,
homeServerUrl = null, homeServerUrl = null,
loginMode = LoginMode.Unknown, loginMode = LoginMode.Unknown,
serverType = ServerType.Unknown,
loginModeSupportedTypes = emptyList() loginModeSupportedTypes = emptyList()
) )
} }
@ -379,9 +399,9 @@ class LoginViewModel @AssistedInject constructor(
when (action.signMode) { when (action.signMode) {
SignMode.SignUp -> startRegistrationFlow() SignMode.SignUp -> startRegistrationFlow()
SignMode.SignIn -> startAuthenticationFlow() SignMode.SignIn -> startAuthenticationFlow()
SignMode.SignInWithMatrixId -> _viewEvents.post(LoginViewEvents.OnSignModeSelected) SignMode.SignInWithMatrixId -> _viewEvents.post(LoginViewEvents.OnSignModeSelected(SignMode.SignInWithMatrixId))
SignMode.Unknown -> Unit SignMode.Unknown -> Unit
}.exhaustive }
} }
private fun handleUpdateServerType(action: LoginAction.UpdateServerType) { private fun handleUpdateServerType(action: LoginAction.UpdateServerType) {
@ -390,6 +410,15 @@ class LoginViewModel @AssistedInject constructor(
serverType = action.serverType serverType = action.serverType
) )
} }
when (action.serverType) {
ServerType.Unknown -> Unit /* Should not happen */
ServerType.MatrixOrg ->
// Request login flow here
handle(LoginAction.UpdateHomeServer(matrixOrgUrl))
ServerType.Modular,
ServerType.Other -> _viewEvents.post(LoginViewEvents.OnServerSelectionDone(action.serverType))
}.exhaustive
} }
private fun handleInitWith(action: LoginAction.InitWith) { private fun handleInitWith(action: LoginAction.InitWith) {
@ -427,7 +456,6 @@ class LoginViewModel @AssistedInject constructor(
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
// TODO Handled JobCancellationException
setState { setState {
copy( copy(
asyncResetPassword = Fail(failure) asyncResetPassword = Fail(failure)
@ -469,7 +497,6 @@ class LoginViewModel @AssistedInject constructor(
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
// TODO Handled JobCancellationException
setState { setState {
copy( copy(
asyncResetMailConfirmed = Fail(failure) asyncResetMailConfirmed = Fail(failure)
@ -485,23 +512,22 @@ class LoginViewModel @AssistedInject constructor(
SignMode.Unknown -> error("Developer error, invalid sign mode") SignMode.Unknown -> error("Developer error, invalid sign mode")
SignMode.SignIn -> handleLogin(action) SignMode.SignIn -> handleLogin(action)
SignMode.SignUp -> handleRegisterWith(action) SignMode.SignUp -> handleRegisterWith(action)
SignMode.SignInWithMatrixId -> handleDirectLogin(action) SignMode.SignInWithMatrixId -> handleDirectLogin(action, null)
}.exhaustive }.exhaustive
} }
private fun handleDirectLogin(action: LoginAction.LoginOrRegister) { private fun handleDirectLogin(action: LoginAction.LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?) {
setState { setState {
copy( copy(
asyncLoginAction = Loading() asyncLoginAction = Loading()
) )
} }
// TODO Handle certificate error in this case. Direct login is deactivated now, so we will handle that later authenticationService.getWellKnownData(action.username, homeServerConnectionConfig, object : MatrixCallback<WellknownResult> {
authenticationService.getWellKnownData(action.username, null, object : MatrixCallback<WellknownResult> {
override fun onSuccess(data: WellknownResult) { override fun onSuccess(data: WellknownResult) {
when (data) { when (data) {
is WellknownResult.Prompt -> is WellknownResult.Prompt ->
onWellknownSuccess(action, data) onWellknownSuccess(action, data, homeServerConnectionConfig)
is WellknownResult.InvalidMatrixId -> { is WellknownResult.InvalidMatrixId -> {
setState { setState {
copy( copy(
@ -522,23 +548,26 @@ class LoginViewModel @AssistedInject constructor(
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
setState { onDirectLoginError(failure)
copy(
asyncLoginAction = Fail(failure)
)
}
} }
}) })
} }
private fun onWellknownSuccess(action: LoginAction.LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt) { private fun onWellknownSuccess(action: LoginAction.LoginOrRegister,
val homeServerConnectionConfig = HomeServerConnectionConfig( wellKnownPrompt: WellknownResult.Prompt,
homeServerUri = Uri.parse(wellKnownPrompt.homeServerUrl), homeServerConnectionConfig: HomeServerConnectionConfig?) {
identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) } val alteredHomeServerConnectionConfig = homeServerConnectionConfig
) ?.copy(
homeServerUri = Uri.parse(wellKnownPrompt.homeServerUrl),
identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) }
)
?: HomeServerConnectionConfig(
homeServerUri = Uri.parse(wellKnownPrompt.homeServerUrl),
identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) }
)
authenticationService.directAuthentication( authenticationService.directAuthentication(
homeServerConnectionConfig, alteredHomeServerConnectionConfig,
action.username, action.username,
action.password, action.password,
action.initialDeviceName, action.initialDeviceName,
@ -548,15 +577,29 @@ class LoginViewModel @AssistedInject constructor(
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
setState { onDirectLoginError(failure)
copy(
asyncLoginAction = Fail(failure)
)
}
} }
}) })
} }
private fun onDirectLoginError(failure: Throwable) {
if (failure is Failure.UnrecognizedCertificateFailure) {
// Display this error in a dialog
_viewEvents.post(LoginViewEvents.Failure(failure))
setState {
copy(
asyncLoginAction = Uninitialized
)
}
} else {
setState {
copy(
asyncLoginAction = Fail(failure)
)
}
}
}
private fun handleLogin(action: LoginAction.LoginOrRegister) { private fun handleLogin(action: LoginAction.LoginOrRegister) {
val safeLoginWizard = loginWizard val safeLoginWizard = loginWizard
@ -584,7 +627,6 @@ class LoginViewModel @AssistedInject constructor(
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
// TODO Handled JobCancellationException
setState { setState {
copy( copy(
asyncLoginAction = Fail(failure) asyncLoginAction = Fail(failure)
@ -609,7 +651,7 @@ class LoginViewModel @AssistedInject constructor(
// Ensure Wizard is ready // Ensure Wizard is ready
loginWizard loginWizard
_viewEvents.post(LoginViewEvents.OnSignModeSelected) _viewEvents.post(LoginViewEvents.OnSignModeSelected(SignMode.SignIn))
} }
private fun onFlowResponse(flowResult: FlowResult) { private fun onFlowResponse(flowResult: FlowResult) {
@ -673,7 +715,10 @@ class LoginViewModel @AssistedInject constructor(
setState { setState {
copy( copy(
asyncHomeServerLoginFlowRequest = Loading() asyncHomeServerLoginFlowRequest = Loading(),
// If user has entered https://matrix.org, ensure that server type is ServerType.MatrixOrg
// It is also useful to set the value again in the case of a certificate error on matrix.org
serverType = if (homeServerConnectionConfig.homeServerUri.toString() == matrixOrgUrl) ServerType.MatrixOrg else serverType
) )
} }
@ -682,7 +727,9 @@ class LoginViewModel @AssistedInject constructor(
_viewEvents.post(LoginViewEvents.Failure(failure)) _viewEvents.post(LoginViewEvents.Failure(failure))
setState { setState {
copy( copy(
asyncHomeServerLoginFlowRequest = Uninitialized asyncHomeServerLoginFlowRequest = Uninitialized,
// If we were trying to retrieve matrix.org login flow, also reset the serverType
serverType = if (serverType == ServerType.MatrixOrg) ServerType.Unknown else serverType
) )
} }
} }

View File

@ -35,7 +35,7 @@ data class LoginViewState(
// User choices // User choices
@PersistState @PersistState
val serverType: ServerType = ServerType.MatrixOrg, val serverType: ServerType = ServerType.Unknown,
@PersistState @PersistState
val signMode: SignMode = SignMode.Unknown, val signMode: SignMode = SignMode.Unknown,
@PersistState @PersistState

View File

@ -17,6 +17,7 @@
package im.vector.riotx.features.login package im.vector.riotx.features.login
enum class ServerType { enum class ServerType {
Unknown,
MatrixOrg, MatrixOrg,
Modular, Modular,
Other Other

View File

@ -205,7 +205,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
@SuppressLint("NewApi") @SuppressLint("NewApi")
fun buildForegroundServiceNotification(@StringRes subTitleResId: Int, withProgress: Boolean = true): Notification { fun buildForegroundServiceNotification(@StringRes subTitleResId: Int, withProgress: Boolean = true): Notification {
// build the pending intent go to the home screen if this is clicked. // build the pending intent go to the home screen if this is clicked.
val i = Intent(context, HomeActivity::class.java) val i = HomeActivity.newIntent(context)
i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
val pi = PendingIntent.getActivity(context, 0, i, 0) val pi = PendingIntent.getActivity(context, 0, i, 0)
@ -307,7 +307,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
val contentPendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), contentIntent, 0) val contentPendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), contentIntent, 0)
val answerCallPendingIntent = TaskStackBuilder.create(context) val answerCallPendingIntent = TaskStackBuilder.create(context)
.addNextIntentWithParentStack(Intent(context, HomeActivity::class.java)) .addNextIntentWithParentStack(HomeActivity.newIntent(context))
.addNextIntent(VectorCallActivity.newIntent( .addNextIntent(VectorCallActivity.newIntent(
context = context, context = context,
callId = callId, callId = callId,
@ -459,7 +459,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
) )
val contentPendingIntent = TaskStackBuilder.create(context) val contentPendingIntent = TaskStackBuilder.create(context)
.addNextIntentWithParentStack(Intent(context, HomeActivity::class.java)) .addNextIntentWithParentStack(HomeActivity.newIntent(context))
// TODO other userId // TODO other userId
.addNextIntent(VectorCallActivity.newIntent(context, callId, roomId, "otherUserId", true, isVideo, null)) .addNextIntent(VectorCallActivity.newIntent(context, callId, roomId, "otherUserId", true, isVideo, null))
.getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT) .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT)
@ -651,7 +651,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
stringProvider.getString(R.string.join), stringProvider.getString(R.string.join),
joinIntentPendingIntent) joinIntentPendingIntent)
val contentIntent = Intent(context, HomeActivity::class.java) val contentIntent = HomeActivity.newIntent(context)
contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
// pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
contentIntent.data = Uri.parse("foobar://" + inviteNotifiableEvent.eventId) contentIntent.data = Uri.parse("foobar://" + inviteNotifiableEvent.eventId)
@ -689,7 +689,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
.setColor(accentColor) .setColor(accentColor)
.setAutoCancel(true) .setAutoCancel(true)
.apply { .apply {
val contentIntent = Intent(context, HomeActivity::class.java) val contentIntent = HomeActivity.newIntent(context)
contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
// pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
contentIntent.data = Uri.parse("foobar://" + simpleNotifiableEvent.eventId) contentIntent.data = Uri.parse("foobar://" + simpleNotifiableEvent.eventId)
@ -718,7 +718,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
// Recreate the back stack // Recreate the back stack
return TaskStackBuilder.create(context) return TaskStackBuilder.create(context)
.addNextIntentWithParentStack(Intent(context, HomeActivity::class.java)) .addNextIntentWithParentStack(HomeActivity.newIntent(context))
.addNextIntent(roomIntentTap) .addNextIntent(roomIntentTap)
.getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT) .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT)
} }

View File

@ -93,8 +93,9 @@ class SoftLogoutFragment @Inject constructor(
softLogoutViewModel.handle(SoftLogoutAction.SignInAgain(password)) softLogoutViewModel.handle(SoftLogoutAction.SignInAgain(password))
} }
override fun signinFallbackSubmit() { override fun signinFallbackSubmit() = withState(loginViewModel) { state ->
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnSignModeSelected)) // The loginViewModel has been prepared for a SSO/login fallback recovery (above)
loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnSignModeSelected(state.signMode)))
} }
override fun clearData() { override fun clearData() {

View File

@ -2,6 +2,7 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/bg_login_server_checked" android:state_checked="true" /> <item android:drawable="@drawable/bg_login_server_checked" android:state_checked="true" />
<item android:drawable="@drawable/bg_login_server_checked" android:state_pressed="true" />
<item android:drawable="@drawable/bg_login_server" /> <item android:drawable="@drawable/bg_login_server" />

View File

@ -106,6 +106,16 @@
</FrameLayout> </FrameLayout>
<TextView
android:id="@+id/loginPasswordNotice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start"
android:text="@string/login_signin_matrix_id_password_notice"
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small"
android:visibility="gone"
tools:visibility="visible" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -43,6 +43,7 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginServerTitle" /> app:layout_constraintTop_toBottomOf="@+id/loginServerTitle" />
<!-- Use a CheckableConstraintLayout to keep the pressed state when retrieving login flow -->
<im.vector.riotx.core.platform.CheckableConstraintLayout <im.vector.riotx.core.platform.CheckableConstraintLayout
android:id="@+id/loginServerChoiceMatrixOrg" android:id="@+id/loginServerChoiceMatrixOrg"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -84,7 +85,7 @@
</im.vector.riotx.core.platform.CheckableConstraintLayout> </im.vector.riotx.core.platform.CheckableConstraintLayout>
<im.vector.riotx.core.platform.CheckableConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/loginServerChoiceModular" android:id="@+id/loginServerChoiceModular"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -135,9 +136,9 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/loginServerChoiceModularText" /> app:layout_constraintTop_toTopOf="@+id/loginServerChoiceModularText" />
</im.vector.riotx.core.platform.CheckableConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<im.vector.riotx.core.platform.CheckableConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/loginServerChoiceOther" android:id="@+id/loginServerChoiceOther"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -178,45 +179,20 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginServerChoiceOtherTitle" /> app:layout_constraintTop_toBottomOf="@+id/loginServerChoiceOtherTitle" />
</im.vector.riotx.core.platform.CheckableConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/loginServerSubmit"
style="@style/Style.Vector.Login.Button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/login_continue"
android:transitionName="loginSubmitTransition"
app:layout_constraintBottom_toTopOf="@+id/loginServerIKnowMyIdNotice"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginServerChoiceOther" />
<TextView
android:id="@+id/loginServerIKnowMyIdNotice"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:gravity="start"
android:text="@string/login_connect_using_matrix_id_notice"
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginServerSubmit" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/loginServerIKnowMyIdSubmit" android:id="@+id/loginServerIKnowMyIdSubmit"
style="@style/Style.Vector.Login.Button.Text" style="@style/Style.Vector.Login.Button.Text"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
android:text="@string/login_connect_using_matrix_id_submit" android:text="@string/login_connect_using_matrix_id_submit"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginServerIKnowMyIdNotice" /> app:layout_constraintTop_toBottomOf="@+id/loginServerChoiceOther" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1995,10 +1995,11 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
</plurals> </plurals>
<string name="login_connect_using_matrix_id_notice">Alternatively, if you already have an account and you know your Matrix identifier and your password, you can use this method:</string> <string name="login_connect_using_matrix_id_notice">Alternatively, if you already have an account and you know your Matrix identifier and your password, you can use this method:</string>
<string name="login_connect_using_matrix_id_submit">Sign in with my Matrix identifier</string> <string name="login_connect_using_matrix_id_submit">Sign in with Matrix ID</string>
<string name="login_signin_matrix_id_title">Sign in</string> <string name="login_signin_matrix_id_title">Sign in with Matrix ID</string>
<string name="login_signin_matrix_id_notice">Enter your identifier and your password</string> <string name="login_signin_matrix_id_notice">If you set up an account on a homeserver, use your Matrix ID (e.g. @user:domain.com) and password below.</string>
<string name="login_signin_matrix_id_hint">User identifier</string> <string name="login_signin_matrix_id_hint">Matrix ID</string>
<string name="login_signin_matrix_id_password_notice">If you dont know your password, go back to reset it.</string>
<string name="login_signin_matrix_id_error_invalid_matrix_id">This is not a valid user identifier. Expected format: \'@user:homeserver.org\'</string> <string name="login_signin_matrix_id_error_invalid_matrix_id">This is not a valid user identifier. Expected format: \'@user:homeserver.org\'</string>
<string name="autodiscover_well_known_error">Unable to find a valid homeserver. Please check your identifier</string> <string name="autodiscover_well_known_error">Unable to find a valid homeserver. Please check your identifier</string>