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

View File

@ -10,7 +10,7 @@ buildscript {
}
}
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.airbnb.okreplay:gradle-plugin:1.5.0"
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
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
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
-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"
xmlns:tools="http://schemas.android.com/tools"
package="im.vector.matrix.android">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@ -8,11 +7,6 @@
<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.
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 im.vector.matrix.android.api.auth.data.Credentials
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.internal.auth.AuthAPI
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.executeRequest
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 okhttp3.OkHttpClient
import javax.inject.Inject
@ -49,13 +51,28 @@ internal class DefaultDirectLoginTask @Inject constructor(
override suspend fun execute(params: DirectLoginTask.Params): Session {
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)
val loginParams = PasswordLoginParams.userIdentifier(params.userId, params.password, params.deviceName)
val credentials = executeRequest<Credentials>(null) {
apiCall = authAPI.login(loginParams)
val credentials = try {
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)

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.rest.DeviceInfo
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.toRest
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.TaskThread
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.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.fetchCopied
@ -340,11 +340,14 @@ internal class DefaultCryptoService @Inject constructor(
}
fun ensureDevice() {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
cryptoCoroutineScope.launchToCallback(coroutineDispatchers.crypto, NoOpMatrixCallback()) {
// Open the store
cryptoStore.open()
// TODO why do that everytime? we should mark that it was done
uploadDeviceKeys()
// this can throw if no network
tryThis {
uploadDeviceKeys()
}
oneTimeKeysUploader.maybeUploadOneTimeKeys()
// this can throw if no backup
tryThis {
@ -389,7 +392,7 @@ internal class DefaultCryptoService @Inject constructor(
// } else {
// Why would we do that? it will be called at end of syn
incomingGossipingRequestManager.processReceivedGossipingRequests()
incomingGossipingRequestManager.processReceivedGossipingRequests()
// }
}.fold(
{
@ -888,7 +891,7 @@ internal class DefaultCryptoService @Inject constructor(
*/
private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean {
return when (secretName) {
MASTER_KEY_SSSS_NAME -> {
MASTER_KEY_SSSS_NAME -> {
crossSigningService.onSecretMSKGossip(secretValue)
true
}
@ -980,7 +983,11 @@ internal class DefaultCryptoService @Inject constructor(
/**
* 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
// Sign it
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, getMyDevice().signalableJSONDictionary())
@ -991,7 +998,9 @@ internal class DefaultCryptoService @Inject constructor(
)
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 getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
fun getGossipingEventsTrail(): List<Event>
fun setDeviceKeysUploaded(uploaded: Boolean)
fun getDeviceKeysUploaded(): Boolean
}

View File

@ -842,6 +842,18 @@ internal class RealmCryptoStore @Inject constructor(
} ?: 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>) {
doRealmTransaction(realmConfiguration) {
// Reset all

View File

@ -54,7 +54,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
// 0, 1, 2: legacy Riot-Android
// 3: migrate to RiotX schema
// 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) {
@ -70,6 +70,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
if (oldVersion <= 7) migrateTo8(realm)
if (oldVersion <= 8) migrateTo9(realm)
if (oldVersion <= 9) migrateTo10(realm)
if (oldVersion <= 10) migrateTo11(realm)
}
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")
?.transform { obj ->
try {
val oldSerializedData = obj.getString("olmInboundGroupSessionData")
deserializeFromRealm<MXOlmInboundGroupSession2>(oldSerializedData)?.let { mxOlmInboundGroupSession2 ->
val newOlmInboundGroupSessionWrapper2 = OlmInboundGroupSessionWrapper2()
val sessionKey = mxOlmInboundGroupSession2.mSession.sessionIdentifier()
val newOlmInboundGroupSessionWrapper = OlmInboundGroupSessionWrapper(sessionKey, false)
.apply {
olmInboundGroupSession = mxOlmInboundGroupSession2.mSession
roomId = mxOlmInboundGroupSession2.mRoomId
@ -191,7 +193,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
forwardingCurve25519KeyChain = mxOlmInboundGroupSession2.mForwardingCurve25519KeyChain
}
obj.setString("olmInboundGroupSessionData", serializeForRealm(newOlmInboundGroupSessionWrapper2))
obj.setString("olmInboundGroupSessionData", serializeForRealm(newOlmInboundGroupSessionWrapper))
}
} catch (e: Exception) {
Timber.e(e, "Error")
@ -445,4 +447,11 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
.addField(SharedSessionEntityFields.CHAIN_INDEX, Long::class.java)
.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.
var backupVersion: String? = null,
// The device keys has been sent to the homeserver
var deviceKeysSentToServer: Boolean = false,
var xSignMasterPrivateKey: String? = null,
var xSignUserPrivateKey: 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 replyFormatted = REPLY_PATTERN.format(
permalink,
stringProvider.getString(R.string.message_reply_to_prefix),
userLink,
originalEvent.senderInfo.disambiguatedDisplayName,
body.takeFormatted(),
@ -372,7 +371,6 @@ internal class LocalEchoEventFactory @Inject constructor(
val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.root.getClearContent().toModel())
val replyFormatted = REPLY_PATTERN.format(
permalink,
stringProvider.getString(R.string.message_reply_to_prefix),
userLink,
userId,
body.takeFormatted(),
@ -434,10 +432,10 @@ internal class LocalEchoEventFactory @Inject constructor(
TextContent(content.body, formattedText)
}
}
MessageType.MSGTYPE_FILE -> return TextContent(stringProvider.getString(R.string.reply_to_a_file))
MessageType.MSGTYPE_AUDIO -> return TextContent(stringProvider.getString(R.string.reply_to_an_audio_file))
MessageType.MSGTYPE_IMAGE -> return TextContent(stringProvider.getString(R.string.reply_to_an_image))
MessageType.MSGTYPE_VIDEO -> return TextContent(stringProvider.getString(R.string.reply_to_a_video))
MessageType.MSGTYPE_FILE -> return TextContent("sent a file.")
MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.")
MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.")
MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.")
else -> return TextContent(content?.body ?: "")
}
}
@ -489,6 +487,6 @@ internal class LocalEchoEventFactory @Inject constructor(
// </blockquote>
// </mx-reply>
// 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.executeRequest
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.identity.IdentityAuthAPI
import im.vector.matrix.android.internal.task.Task
@ -106,6 +107,12 @@ internal class DefaultGetWellknownTask @Inject constructor(
}
} catch (throwable: Throwable) {
when (throwable) {
is UnrecognizedCertificateException -> {
throw Failure.UnrecognizedCertificateFailure(
"https://$domain",
throwable.fingerprint
)
}
is Failure.NetworkConnection -> {
WellknownResult.Ignore
}

View File

@ -63,12 +63,6 @@
<string name="summary_user_sent_sticker">أرسل %1$s ملصقا.</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_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_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="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_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_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="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_room_invite">Покана за стая</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="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>

View File

@ -46,8 +46,6 @@
<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="message_reply_to_prefix">V odpovědi na</string>
<string name="could_not_redact">Nelze vymazat</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_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_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="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 -->
<string name="room_displayname_invite_from">Einladung von %s</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_error_unkwown_inbound_session_id">Η συσκευή του/της αποστολέα δεν μας έχει στείλει τα κλειδιά για αυτό το μήνυμα.</string>
<string name="message_reply_to_prefix">Προς απάντηση στο</string>
<string name="unable_to_send_message">Αποτυχία αποστολής μηνύματος</string>
<string name="message_failed_to_upload">Αποτυχία αναφόρτωσης εικόνας</string>
@ -56,10 +54,6 @@
<string name="notice_voip_finished">Η VoIP διάσκεψη έληξε</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_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_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="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>
@ -62,11 +60,6 @@
<string name="medium_email">Retpoŝtadreso</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_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="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 -->
<string name="room_displayname_invite_from">Invitación de %s</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="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 -->
<string name="room_displayname_invite_from">Invitación de %s</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_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="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_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_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="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_room_invite">Gela gonbidapena</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_error_unkwown_inbound_session_id">دستگاه فرستنده، کلیدهای این پیام را برایمان نفرستاده است.</string>
<string name="message_reply_to_prefix">در پاسخ به</string>
<string name="unable_to_send_message">ناتوان در فرستادن پیام</string>
<string name="message_failed_to_upload">شکست در بارگذاری تصویر</string>
@ -67,11 +65,6 @@
<string name="medium_email">نشانی رایانامه</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_room_invite">دعوت اتاق</string>

View File

@ -70,13 +70,6 @@
<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">
<item quantity="one">%1$s ja yksi muu</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="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_room_invite">Invitation au salon</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_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="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="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>

View File

@ -62,13 +62,6 @@
<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_room_invite">Meghívó egy szobába</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_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>

View File

@ -62,13 +62,6 @@
<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 -->
<string name="room_displayname_invite_from">Invito da %s</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_error_unkwown_inbound_session_id">送信者の端末からこのメッセージのキーが送信されていません。</string>
<string name="message_reply_to_prefix">に返信</string>
<string name="could_not_redact">修正できませんでした</string>
<string name="unable_to_send_message">メッセージを送信できません</string>
@ -73,9 +71,4 @@
<string name="medium_email">メールアドレス</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>

View File

@ -52,8 +52,6 @@
<string name="notice_crypto_unable_to_decrypt">** 암호를 복호화할 수 없음: %s **</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="unable_to_send_message">메시지를 보낼 수 없습니다</string>
@ -69,11 +67,6 @@
<string name="medium_email">이메일 주소</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_room_invite">방 초대</string>

View File

@ -71,13 +71,6 @@
<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 -->
<string name="room_displayname_invite_from">Uitnodiging van %s</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_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="unable_to_send_message">Fekk ikkje å senda meldingi</string>
@ -64,11 +62,6 @@
<string name="medium_email">Epostadresse</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_room_invite">Rominnbjoding</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_answered_call">%s odebrał(a) połączenie.</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_room_invite">Zaproszenie do pokoju</string>
@ -76,11 +75,6 @@
<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="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_by">Wiadomość usunięta przez %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="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 -->
<string name="room_displayname_invite_from">Convite de %s</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="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 -->
<string name="room_displayname_invite_from">Приглашение от %s</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="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_room_invite">Pozvanie do miestnosti</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_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="unable_to_send_message">Sarrihet të dërgohet mesazh</string>
@ -51,11 +49,6 @@
<string name="medium_email">Adresë email</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_room_invite">Ftesë Dhome</string>

View File

@ -56,8 +56,6 @@
<string name="notice_crypto_unable_to_decrypt">** Неможливо розшифрувати: %s **</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="unable_to_send_message">Не вдалося надіслати повідомлення</string>
@ -71,11 +69,6 @@
<string name="medium_email">Адреса електронної пошти</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">
<item quantity="one">%1$s та 1 інший</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_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="unable_to_send_message">Kosteg t bericht nie verzendn</string>
@ -67,11 +65,6 @@
<string name="medium_email">E-mailadresse</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_room_invite">Gespreksuutnodigienge</string>

View File

@ -63,13 +63,6 @@
<string name="summary_message">%1$s%2$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_invite_from">来自 %s 的邀请</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="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_room_invite">聊天室邀請</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>
<!-- Messages -->
<string name="message_reply_to_prefix">In reply to</string>
<!-- Room Screen -->
<string name="could_not_redact">Could not redact</string>
@ -139,12 +138,6 @@
<string name="medium_email">Email address</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 -->
<string name="room_displayname_invite_from">Invite from %s</string>
<string name="room_displayname_room_invite">Room Invite</string>

View File

@ -17,7 +17,7 @@ androidExtensions {
// Note: 2 digits max for each value
ext.versionMajor = 0
ext.versionMinor = 91
ext.versionPatch = 3
ext.versionPatch = 4
static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct'
@ -106,6 +106,11 @@ def buildNumber = System.env.BUILDKITE_BUILD_NUMBER as Integer ?: 0
android {
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 {
applicationId "im.vector.app"
// Set to API 21: see #405
@ -232,8 +237,7 @@ android {
lintOptions {
lintConfig file("lint.xml")
// TODO Restore true once pb with WorkManager is fixed
abortOnError false
abortOnError true
}
compileOptions {

View File

@ -20,4 +20,7 @@
# hide the original source file name.
#-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 -->
<!-- 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
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileProvider"

View File

@ -63,7 +63,13 @@ import java.util.Locale
import java.util.concurrent.Executors
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
@Inject lateinit var legacySessionImporter: LegacySessionImporter
@ -85,6 +91,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
@Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
lateinit var vectorComponent: VectorComponent
// font thread handler
private var fontThreadHandler: Handler? = null
@ -157,7 +164,11 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
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 {
return vectorComponent

View File

@ -17,29 +17,39 @@
package im.vector.riotx.core.utils
import android.app.Activity
import android.app.DownloadManager
import android.content.ActivityNotFoundException
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.Browser
import android.provider.MediaStore
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.browser.customtabs.CustomTabsIntent
import androidx.browser.customtabs.CustomTabsSession
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.extensions.tryThis
import im.vector.riotx.BuildConfig
import im.vector.riotx.R
import im.vector.riotx.features.notifications.NotificationUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import okio.buffer
import okio.sink
import okio.source
import timber.log.Timber
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.util.Date
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) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val externalContentUri: Uri
val values = ContentValues()
when {
mediaMimeType?.startsWith("image/") == true -> {
externalContentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
values.put(MediaStore.Images.Media.TITLE, title)
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 values = ContentValues().apply {
put(MediaStore.Images.Media.TITLE, title)
put(MediaStore.Images.Media.DISPLAY_NAME, title)
put(MediaStore.Images.Media.MIME_TYPE, mediaMimeType)
put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis())
put(MediaStore.Images.Media.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)
if (uri == null) {
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)
}
}
// TODO add notification?
} else {
@Suppress("DEPRECATION")
Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE).also { mediaScanIntent ->
mediaScanIntent.data = Uri.fromFile(file)
context.sendBroadcast(mediaScanIntent)
saveMediaLegacy(context, mediaMimeType, title, file)
}
}
@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
*/
@ -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
}
}
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) {
super.bind(holder)
holder.textInputLayout.isEnabled = enabled
holder.textInputLayout.hint = hint

View File

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

View File

@ -21,4 +21,6 @@ import javax.inject.Inject
/**
* 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) {
when (throwable) {
is Failure.Cancelled ->
/* Ignore this error, user has cancelled the action */
Unit
is Failure.ServerError ->
if (throwable.error.code == MatrixError.M_FORBIDDEN
&& throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) {

View File

@ -151,8 +151,8 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
// TODO Disabled because it provokes a flickering
// ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
})
is LoginViewEvents.OnServerSelectionDone -> onServerSelectionDone()
is LoginViewEvents.OnSignModeSelected -> onSignModeSelected()
is LoginViewEvents.OnServerSelectionDone -> onServerSelectionDone(loginViewEvents)
is LoginViewEvents.OnSignModeSelected -> onSignModeSelected(loginViewEvents)
is LoginViewEvents.OnLoginFlowRetrieved ->
addFragmentToBackstack(R.id.loginFragmentContainer,
if (loginViewEvents.isSso) {
@ -228,18 +228,20 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
.show()
}
private fun onServerSelectionDone() = withState(loginViewModel) { state ->
when (state.serverType) {
private fun onServerSelectionDone(loginViewEvents: LoginViewEvents.OnServerSelectionDone) {
when (loginViewEvents.serverType) {
ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow
ServerType.Modular,
ServerType.Other -> addFragmentToBackstack(R.id.loginFragmentContainer,
LoginServerUrlFormFragment::class.java,
option = commonOption)
ServerType.Unknown -> Unit /* Should not happen */
}
}
private fun onSignModeSelected() = withState(loginViewModel) { state ->
when (state.signMode) {
private fun onSignModeSelected(loginViewEvents: LoginViewEvents.OnSignModeSelected) = withState(loginViewModel) { state ->
// 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.SignUp -> {
// This is managed by the LoginViewEvents

View File

@ -54,6 +54,7 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
private var passwordShown = false
private var isSignupMode = false
// Temporary patch for https://github.com/vector-im/riotX-android/issues/1410,
// waiting for https://github.com/matrix-org/synapse/issues/7576
private var isNumericOnlyUserIdForbidden = false
@ -138,6 +139,7 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
loginServerIcon.isVisible = false
loginTitle.text = getString(R.string.login_signin_matrix_id_title)
loginNotice.text = getString(R.string.login_signin_matrix_id_notice)
loginPasswordNotice.isVisible = true
} else {
val resId = when (state.signMode) {
SignMode.Unknown -> error("developer error")
@ -164,7 +166,9 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl())
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.view.View
import butterknife.OnClick
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.utils.openUrlInChromeCustomTab
import kotlinx.android.synthetic.main.fragment_login_server_selection.*
@ -40,11 +39,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment
}
private fun updateSelectedChoice(state: LoginViewState) {
state.serverType.let {
loginServerChoiceMatrixOrg.isChecked = it == ServerType.MatrixOrg
loginServerChoiceModular.isChecked = it == ServerType.Modular
loginServerChoiceOther.isChecked = it == ServerType.Other
}
loginServerChoiceMatrixOrg.isChecked = state.serverType == ServerType.MatrixOrg
}
private fun initTextViews() {
@ -61,42 +56,17 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment
@OnClick(R.id.loginServerChoiceMatrixOrg)
fun selectMatrixOrg() {
if (loginServerChoiceMatrixOrg.isChecked) {
// Consider this is a submit
submit()
} else {
loginViewModel.handle(LoginAction.UpdateServerType(ServerType.MatrixOrg))
}
loginViewModel.handle(LoginAction.UpdateServerType(ServerType.MatrixOrg))
}
@OnClick(R.id.loginServerChoiceModular)
fun selectModular() {
if (loginServerChoiceModular.isChecked) {
// Consider this is a submit
submit()
} else {
loginViewModel.handle(LoginAction.UpdateServerType(ServerType.Modular))
}
loginViewModel.handle(LoginAction.UpdateServerType(ServerType.Modular))
}
@OnClick(R.id.loginServerChoiceOther)
fun selectOther() {
if (loginServerChoiceOther.isChecked) {
// 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))
}
loginViewModel.handle(LoginAction.UpdateServerType(ServerType.Other))
}
@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)
loginServerUrlFormNotice.text = getString(R.string.login_server_url_form_modular_notice)
}
ServerType.Other -> {
else -> {
loginServerUrlFormIcon.isVisible = false
loginServerUrlFormTitle.text = getString(R.string.login_server_other_title)
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)
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)
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
object OpenServerSelection : LoginViewEvents()
object OnServerSelectionDone : LoginViewEvents()
data class OnServerSelectionDone(val serverType: ServerType) : LoginViewEvents()
data class OnLoginFlowRetrieved(val isSso: Boolean) : LoginViewEvents()
object OnSignModeSelected : LoginViewEvents()
data class OnSignModeSelected(val signMode: SignMode) : LoginViewEvents()
object OnForgetPasswordClicked : LoginViewEvents()
object OnResetPasswordSendThreePidDone : 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.Stage
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.util.Cancelable
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.platform.VectorViewModel
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.notifications.PushRuleTriggerListener
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 val matrixOrgUrl = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash()
val currentThreePid: String?
get() = registrationWizard?.currentThreePid
@ -111,8 +117,8 @@ class LoginViewModel @AssistedInject constructor(
is LoginAction.UpdateServerType -> handleUpdateServerType(action)
is LoginAction.UpdateSignMode -> handleUpdateSignMode(action)
is LoginAction.InitWith -> handleInitWith(action)
is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action)
is LoginAction.LoginOrRegister -> handleLoginOrRegister(action)
is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action).also { lastAction = action }
is LoginAction.LoginOrRegister -> handleLoginOrRegister(action).also { lastAction = action }
is LoginAction.LoginWithToken -> handleLoginWithToken(action)
is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action)
is LoginAction.ResetPassword -> handleResetPassword(action)
@ -126,10 +132,23 @@ class LoginViewModel @AssistedInject constructor(
}
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
currentHomeServerConnectionConfig
?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) }
?.let { getLoginFlow(it) }
// It happen when we get the login flow, or during direct authentication.
// So alter the homeserver config and retrieve again the login flow
when (val finalLastAction = lastAction) {
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) {
@ -321,7 +340,7 @@ class LoginViewModel @AssistedInject constructor(
LoginAction.ResetHomeServerType -> {
setState {
copy(
serverType = ServerType.MatrixOrg
serverType = ServerType.Unknown
)
}
}
@ -333,6 +352,7 @@ class LoginViewModel @AssistedInject constructor(
asyncHomeServerLoginFlowRequest = Uninitialized,
homeServerUrl = null,
loginMode = LoginMode.Unknown,
serverType = ServerType.Unknown,
loginModeSupportedTypes = emptyList()
)
}
@ -379,9 +399,9 @@ class LoginViewModel @AssistedInject constructor(
when (action.signMode) {
SignMode.SignUp -> startRegistrationFlow()
SignMode.SignIn -> startAuthenticationFlow()
SignMode.SignInWithMatrixId -> _viewEvents.post(LoginViewEvents.OnSignModeSelected)
SignMode.SignInWithMatrixId -> _viewEvents.post(LoginViewEvents.OnSignModeSelected(SignMode.SignInWithMatrixId))
SignMode.Unknown -> Unit
}.exhaustive
}
}
private fun handleUpdateServerType(action: LoginAction.UpdateServerType) {
@ -390,6 +410,15 @@ class LoginViewModel @AssistedInject constructor(
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) {
@ -427,7 +456,6 @@ class LoginViewModel @AssistedInject constructor(
}
override fun onFailure(failure: Throwable) {
// TODO Handled JobCancellationException
setState {
copy(
asyncResetPassword = Fail(failure)
@ -469,7 +497,6 @@ class LoginViewModel @AssistedInject constructor(
}
override fun onFailure(failure: Throwable) {
// TODO Handled JobCancellationException
setState {
copy(
asyncResetMailConfirmed = Fail(failure)
@ -485,23 +512,22 @@ class LoginViewModel @AssistedInject constructor(
SignMode.Unknown -> error("Developer error, invalid sign mode")
SignMode.SignIn -> handleLogin(action)
SignMode.SignUp -> handleRegisterWith(action)
SignMode.SignInWithMatrixId -> handleDirectLogin(action)
SignMode.SignInWithMatrixId -> handleDirectLogin(action, null)
}.exhaustive
}
private fun handleDirectLogin(action: LoginAction.LoginOrRegister) {
private fun handleDirectLogin(action: LoginAction.LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?) {
setState {
copy(
asyncLoginAction = Loading()
)
}
// TODO Handle certificate error in this case. Direct login is deactivated now, so we will handle that later
authenticationService.getWellKnownData(action.username, null, object : MatrixCallback<WellknownResult> {
authenticationService.getWellKnownData(action.username, homeServerConnectionConfig, object : MatrixCallback<WellknownResult> {
override fun onSuccess(data: WellknownResult) {
when (data) {
is WellknownResult.Prompt ->
onWellknownSuccess(action, data)
onWellknownSuccess(action, data, homeServerConnectionConfig)
is WellknownResult.InvalidMatrixId -> {
setState {
copy(
@ -522,23 +548,26 @@ class LoginViewModel @AssistedInject constructor(
}
override fun onFailure(failure: Throwable) {
setState {
copy(
asyncLoginAction = Fail(failure)
)
}
onDirectLoginError(failure)
}
})
}
private fun onWellknownSuccess(action: LoginAction.LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt) {
val homeServerConnectionConfig = HomeServerConnectionConfig(
homeServerUri = Uri.parse(wellKnownPrompt.homeServerUrl),
identityServerUri = wellKnownPrompt.identityServerUrl?.let { Uri.parse(it) }
)
private fun onWellknownSuccess(action: LoginAction.LoginOrRegister,
wellKnownPrompt: WellknownResult.Prompt,
homeServerConnectionConfig: HomeServerConnectionConfig?) {
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(
homeServerConnectionConfig,
alteredHomeServerConnectionConfig,
action.username,
action.password,
action.initialDeviceName,
@ -548,15 +577,29 @@ class LoginViewModel @AssistedInject constructor(
}
override fun onFailure(failure: Throwable) {
setState {
copy(
asyncLoginAction = Fail(failure)
)
}
onDirectLoginError(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) {
val safeLoginWizard = loginWizard
@ -584,7 +627,6 @@ class LoginViewModel @AssistedInject constructor(
}
override fun onFailure(failure: Throwable) {
// TODO Handled JobCancellationException
setState {
copy(
asyncLoginAction = Fail(failure)
@ -609,7 +651,7 @@ class LoginViewModel @AssistedInject constructor(
// Ensure Wizard is ready
loginWizard
_viewEvents.post(LoginViewEvents.OnSignModeSelected)
_viewEvents.post(LoginViewEvents.OnSignModeSelected(SignMode.SignIn))
}
private fun onFlowResponse(flowResult: FlowResult) {
@ -673,7 +715,10 @@ class LoginViewModel @AssistedInject constructor(
setState {
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))
setState {
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
@PersistState
val serverType: ServerType = ServerType.MatrixOrg,
val serverType: ServerType = ServerType.Unknown,
@PersistState
val signMode: SignMode = SignMode.Unknown,
@PersistState

View File

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

View File

@ -205,7 +205,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
@SuppressLint("NewApi")
fun buildForegroundServiceNotification(@StringRes subTitleResId: Int, withProgress: Boolean = true): Notification {
// 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
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 answerCallPendingIntent = TaskStackBuilder.create(context)
.addNextIntentWithParentStack(Intent(context, HomeActivity::class.java))
.addNextIntentWithParentStack(HomeActivity.newIntent(context))
.addNextIntent(VectorCallActivity.newIntent(
context = context,
callId = callId,
@ -459,7 +459,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
)
val contentPendingIntent = TaskStackBuilder.create(context)
.addNextIntentWithParentStack(Intent(context, HomeActivity::class.java))
.addNextIntentWithParentStack(HomeActivity.newIntent(context))
// TODO other userId
.addNextIntent(VectorCallActivity.newIntent(context, callId, roomId, "otherUserId", true, isVideo, null))
.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),
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
// 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)
@ -689,7 +689,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
.setColor(accentColor)
.setAutoCancel(true)
.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
// 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)
@ -718,7 +718,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
// Recreate the back stack
return TaskStackBuilder.create(context)
.addNextIntentWithParentStack(Intent(context, HomeActivity::class.java))
.addNextIntentWithParentStack(HomeActivity.newIntent(context))
.addNextIntent(roomIntentTap)
.getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT)
}

View File

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

View File

@ -2,6 +2,7 @@
<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_pressed="true" />
<item android:drawable="@drawable/bg_login_server" />

View File

@ -106,6 +106,16 @@
</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
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@ -43,6 +43,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginServerTitle" />
<!-- Use a CheckableConstraintLayout to keep the pressed state when retrieving login flow -->
<im.vector.riotx.core.platform.CheckableConstraintLayout
android:id="@+id/loginServerChoiceMatrixOrg"
android:layout_width="match_parent"
@ -84,7 +85,7 @@
</im.vector.riotx.core.platform.CheckableConstraintLayout>
<im.vector.riotx.core.platform.CheckableConstraintLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/loginServerChoiceModular"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -135,9 +136,9 @@
app:layout_constraintEnd_toEndOf="parent"
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:layout_width="match_parent"
android:layout_height="wrap_content"
@ -178,45 +179,20 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginServerChoiceOtherTitle" />
</im.vector.riotx.core.platform.CheckableConstraintLayout>
<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" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/loginServerIKnowMyIdSubmit"
style="@style/Style.Vector.Login.Button.Text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
android:text="@string/login_connect_using_matrix_id_submit"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginServerIKnowMyIdNotice" />
app:layout_constraintTop_toBottomOf="@+id/loginServerChoiceOther" />
</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>
<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_signin_matrix_id_title">Sign in</string>
<string name="login_signin_matrix_id_notice">Enter your identifier and your password</string>
<string name="login_signin_matrix_id_hint">User 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 with Matrix ID</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">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="autodiscover_well_known_error">Unable to find a valid homeserver. Please check your identifier</string>