[issue-2610] Merge branch 'develop' of https://github.com/vector-im/element-android into feature/issue-2610-override-nick-color-via-user-account-data

This commit is contained in:
Péter Radics 2021-02-03 20:27:27 +01:00
commit 0109cdefa6
158 changed files with 3156 additions and 1238 deletions

View file

@ -1,4 +1,4 @@
Changes in Element 1.0.15 (2020-XX-XX) Changes in Element 1.0.16 (2020-XX-XX)
=================================================== ===================================================
Features ✨: Features ✨:
@ -23,6 +23,36 @@ Build 🧱:
Test: Test:
- -
Other changes:
-
Changes in Element 1.0.15 (2020-02-03)
===================================================
Features ✨:
- Social Login support
Improvements 🙌:
- SSO support for cross signing (#1062)
- Deactivate account when logged in with SSO (#1264)
- SSO UIA doesn't work (#2754)
Bugfix 🐛:
- Fix clear cache issue: sometimes, after a clear cache, there is still a token, so the init sync service is not started.
- Sidebar too large in horizontal orientation or tablets (#475)
- UrlPreview should be updated when the url is edited and changed (#2678)
- When receiving a new pepper from identity server, use it on the next hash lookup (#2708)
- Crashes reported by PlayStore (new in 1.0.14) (#2707)
- Widgets: Support $matrix_widget_id parameter (#2748)
- Data for Worker overload (#2721)
- Fix multiple tasks
SDK API changes ⚠️:
- Increase targetSdkVersion to 30 (#2600)
Build 🧱:
- Compile with Android SDK 30 (Android 11)
Other changes: Other changes:
- Update Dagger to 2.31 version so we can use the embedded AssistedInject feature - Update Dagger to 2.31 version so we can use the embedded AssistedInject feature

View file

@ -32,11 +32,11 @@ buildscript {
} }
android { android {
compileSdkVersion 29 compileSdkVersion 30
defaultConfig { defaultConfig {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 30
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"
} }

View file

@ -18,15 +18,19 @@
package im.vector.lib.attachmentviewer package im.vector.lib.attachmentviewer
import android.graphics.Color import android.graphics.Color
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.GestureDetector import android.view.GestureDetector
import android.view.MotionEvent import android.view.MotionEvent
import android.view.ScaleGestureDetector import android.view.ScaleGestureDetector
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.view.WindowManager import android.view.WindowManager
import android.widget.ImageView import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.GestureDetectorCompat import androidx.core.view.GestureDetectorCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -94,14 +98,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// This is important for the dispatchTouchEvent, if not we must correct setDecorViewFullScreen()
// the touch coordinates
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_IMMERSIVE)
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
views = ActivityAttachmentViewerBinding.inflate(layoutInflater) views = ActivityAttachmentViewerBinding.inflate(layoutInflater)
setContentView(views.root) setContentView(views.root)
@ -134,6 +131,29 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
} }
} }
@Suppress("DEPRECATION")
private fun setDecorViewFullScreen() {
// This is important for the dispatchTouchEvent, if not we must correct
// the touch coordinates
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
window.setDecorFitsSystemWindows(false)
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
// New API instead of FLAG_TRANSLUCENT_STATUS
window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
// new API instead of FLAG_TRANSLUCENT_NAVIGATION
window.navigationBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
} else {
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_IMMERSIVE)
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
}
}
fun onSelectedPositionChanged(position: Int) { fun onSelectedPositionChanged(position: Int) {
attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition)?.let { attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition)?.let {
(it as? BaseViewHolder)?.onSelected(false) (it as? BaseViewHolder)?.onSelected(false)
@ -313,11 +333,24 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
?.handleCommand(commands) ?.handleCommand(commands)
} }
@Suppress("DEPRECATION")
private fun hideSystemUI() { private fun hideSystemUI() {
systemUiVisibility = false systemUiVisibility = false
// Enables regular immersive mode. // Enables regular immersive mode.
// For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE. // For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE.
// Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY // Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
window.setDecorFitsSystemWindows(false)
// new API instead of SYSTEM_UI_FLAG_HIDE_NAVIGATION
window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars())
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
// New API instead of FLAG_TRANSLUCENT_STATUS
window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
// New API instead of FLAG_TRANSLUCENT_NAVIGATION
window.navigationBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
} else {
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
// Set the content to appear under the system bars so that the // Set the content to appear under the system bars so that the
// content doesn't resize when the system bars hide and show. // content doesn't resize when the system bars hide and show.
@ -328,13 +361,20 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN) or View.SYSTEM_UI_FLAG_FULLSCREEN)
} }
}
// Shows the system bars by removing all the flags // Shows the system bars by removing all the flags
// except for the ones that make the content appear under the system bars. // except for the ones that make the content appear under the system bars.
@Suppress("DEPRECATION")
private fun showSystemUI() { private fun showSystemUI() {
systemUiVisibility = true systemUiVisibility = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
window.setDecorFitsSystemWindows(false)
} else {
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
} }
}
} }

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<color name="half_transparent_status_bar">#80000000</color>
</resources>

View file

@ -0,0 +1,2 @@
Main changes in this version: Social Login support.
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.0.15

View file

@ -1,7 +1,6 @@
#Fri Jan 15 11:30:47 CET 2021
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
#distributionSha256Sum=a7ca23b3ccf265680f2bfd35f1f00b1424f4466292c7337c85d46c9641b3f053 distributionSha256Sum=3db89524a3981819ff28c3f979236c1274a726e146ced0c8a2020417f9bc0782
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-all.zip

View file

@ -3,11 +3,11 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
android { android {
compileSdkVersion 29 compileSdkVersion 30
defaultConfig { defaultConfig {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 30
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"

View file

@ -14,12 +14,12 @@ buildscript {
} }
android { android {
compileSdkVersion 29 compileSdkVersion 30
testOptions.unitTests.includeAndroidResources = true testOptions.unitTests.includeAndroidResources = true
defaultConfig { defaultConfig {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 30
versionCode 1 versionCode 1
versionName "0.0.1" versionName "0.0.1"
// Multidex is useful for tests // Multidex is useful for tests

View file

@ -16,8 +16,18 @@
package org.matrix.android.sdk.account package org.matrix.android.sdk.account
import org.junit.Assert.assertTrue
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.data.LoginFlowResult import org.matrix.android.sdk.api.auth.data.LoginFlowResult
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.auth.registration.RegistrationResult import org.matrix.android.sdk.api.auth.registration.RegistrationResult
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.MatrixError
@ -25,12 +35,8 @@ import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.common.TestMatrixCallback import org.matrix.android.sdk.common.TestMatrixCallback
import org.junit.Assert.assertTrue import kotlin.coroutines.Continuation
import org.junit.FixMethodOrder import kotlin.coroutines.resume
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
@RunWith(JUnit4::class) @RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@ -44,7 +50,18 @@ class DeactivateAccountTest : InstrumentedTest {
// Deactivate the account // Deactivate the account
commonTestHelper.runBlockingTest { commonTestHelper.runBlockingTest {
session.deactivateAccount(TestConstants.PASSWORD, false) session.deactivateAccount(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = session.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, false)
} }
// Try to login on the previous account, it will fail (M_USER_DEACTIVATED) // Try to login on the previous account, it will fail (M_USER_DEACTIVATED)

View file

@ -378,7 +378,9 @@ class CommonTestHelper(context: Context) {
fun Iterable<Session>.signOutAndClose() = forEach { signOutAndClose(it) } fun Iterable<Session>.signOutAndClose() = forEach { signOutAndClose(it) }
fun signOutAndClose(session: Session) { fun signOutAndClose(session: Session) {
doSync<Unit>(60_000) { session.signOut(true, it) } runBlockingTest(timeout = 60_000) {
session.signOut(true)
}
// no need signout will close // no need signout will close
// session.close() // session.close()
} }

View file

@ -19,6 +19,18 @@ package org.matrix.android.sdk.common
import android.os.SystemClock import android.os.SystemClock
import android.util.Log import android.util.Log
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
@ -36,17 +48,10 @@ import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import java.util.UUID import java.util.UUID
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
@ -304,10 +309,18 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
fun initializeCrossSigning(session: Session) { fun initializeCrossSigning(session: Session) {
mTestHelper.doSync<Unit> { mTestHelper.doSync<Unit> {
session.cryptoService().crossSigningService() session.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth( .initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = session.myUserId, user = session.myUserId,
password = TestConstants.PASSWORD password = TestConstants.PASSWORD,
), it) session = flowResponse.session
)
)
}
}, it)
} }
} }

View file

@ -17,7 +17,18 @@
package org.matrix.android.sdk.internal.crypto package org.matrix.android.sdk.internal.crypto
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.amshove.kluent.shouldBe
import org.junit.Assert
import org.junit.Before
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
@ -30,19 +41,13 @@ import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm
import org.amshove.kluent.shouldBe
import org.junit.Assert
import org.junit.Before
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.olm.OlmSession import org.matrix.olm.OlmSession
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
/** /**
* Ref: * Ref:
@ -202,10 +207,18 @@ class UnwedgingTest : InstrumentedTest {
// It's a trick to force key request on fail to decrypt // It's a trick to force key request on fail to decrypt
mTestHelper.doSync<Unit> { mTestHelper.doSync<Unit> {
bobSession.cryptoService().crossSigningService() bobSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth( .initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = bobSession.myUserId, user = bobSession.myUserId,
password = TestConstants.PASSWORD password = TestConstants.PASSWORD,
), it) session = flowResponse.session
)
)
}
}, it)
} }
// Wait until we received back the key // Wait until we received back the key

View file

@ -17,14 +17,6 @@
package org.matrix.android.sdk.internal.crypto.crosssigning package org.matrix.android.sdk.internal.crypto.crosssigning
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
@ -35,6 +27,19 @@ import org.junit.FixMethodOrder
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.MethodSorters import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING) @FixMethodOrder(MethodSorters.NAME_ASCENDING)
@ -49,10 +54,17 @@ class XSigningTest : InstrumentedTest {
mTestHelper.doSync<Unit> { mTestHelper.doSync<Unit> {
aliceSession.cryptoService().crossSigningService() aliceSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth( .initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = aliceSession.myUserId, user = aliceSession.myUserId,
password = TestConstants.PASSWORD password = TestConstants.PASSWORD,
), it) session = flowResponse.session
)
)
}
}, it)
} }
val myCrossSigningKeys = aliceSession.cryptoService().crossSigningService().getMyCrossSigningKeys() val myCrossSigningKeys = aliceSession.cryptoService().crossSigningService().getMyCrossSigningKeys()
@ -86,8 +98,18 @@ class XSigningTest : InstrumentedTest {
password = TestConstants.PASSWORD password = TestConstants.PASSWORD
) )
mTestHelper.doSync<Unit> { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(aliceAuthParams, it) } mTestHelper.doSync<Unit> {
mTestHelper.doSync<Unit> { bobSession.cryptoService().crossSigningService().initializeCrossSigning(bobAuthParams, it) } aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(aliceAuthParams)
}
}, it)
}
mTestHelper.doSync<Unit> { bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(bobAuthParams)
}
}, it) }
// Check that alice can see bob keys // Check that alice can see bob keys
mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) } mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) }
@ -122,8 +144,16 @@ class XSigningTest : InstrumentedTest {
password = TestConstants.PASSWORD password = TestConstants.PASSWORD
) )
mTestHelper.doSync<Unit> { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(aliceAuthParams, it) } mTestHelper.doSync<Unit> { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
mTestHelper.doSync<Unit> { bobSession.cryptoService().crossSigningService().initializeCrossSigning(bobAuthParams, it) } override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(aliceAuthParams)
}
}, it) }
mTestHelper.doSync<Unit> { bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(bobAuthParams)
}
}, it) }
// Check that alice can see bob keys // Check that alice can see bob keys
val bobUserId = bobSession.myUserId val bobUserId = bobSession.myUserId

View file

@ -18,7 +18,21 @@ package org.matrix.android.sdk.internal.crypto.gossiping
import android.util.Log import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue
import junit.framework.TestCase.fail
import org.junit.Assert
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
@ -28,6 +42,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxStat
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.SessionTestParams
@ -40,19 +55,9 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue
import junit.framework.TestCase.fail
import org.junit.Assert
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@ -200,10 +205,17 @@ class KeyShareTests : InstrumentedTest {
mTestHelper.doSync<Unit> { mTestHelper.doSync<Unit> {
aliceSession1.cryptoService().crossSigningService() aliceSession1.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth( .initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = aliceSession1.myUserId, user = aliceSession1.myUserId,
password = TestConstants.PASSWORD password = TestConstants.PASSWORD
), it) )
)
}
}, it)
} }
// Also bootstrap keybackup on first session // Also bootstrap keybackup on first session
@ -305,10 +317,18 @@ class KeyShareTests : InstrumentedTest {
mTestHelper.doSync<Unit> { mTestHelper.doSync<Unit> {
aliceSession.cryptoService().crossSigningService() aliceSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth( .initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = aliceSession.myUserId, user = aliceSession.myUserId,
password = TestConstants.PASSWORD password = TestConstants.PASSWORD,
), it) session = flowResponse.session
)
)
}
}, it)
} }
// Create an encrypted room and send a couple of messages // Create an encrypted room and send a couple of messages
@ -332,10 +352,18 @@ class KeyShareTests : InstrumentedTest {
val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(true)) val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(true))
mTestHelper.doSync<Unit> { mTestHelper.doSync<Unit> {
bobSession.cryptoService().crossSigningService() bobSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth( .initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = bobSession.myUserId, user = bobSession.myUserId,
password = TestConstants.PASSWORD password = TestConstants.PASSWORD,
), it) session = flowResponse.session
)
)
}
}, it)
} }
// Let alice invite bob // Let alice invite bob

View file

@ -17,20 +17,25 @@
package org.matrix.android.sdk.internal.crypto.verification.qrcode package org.matrix.android.sdk.internal.crypto.verification.qrcode
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBe
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.MethodSorters import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.TestConstants
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@ -157,18 +162,34 @@ class VerificationTest : InstrumentedTest {
mTestHelper.doSync<Unit> { callback -> mTestHelper.doSync<Unit> { callback ->
aliceSession.cryptoService().crossSigningService() aliceSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth( .initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = aliceSession.myUserId, user = aliceSession.myUserId,
password = TestConstants.PASSWORD password = TestConstants.PASSWORD,
), callback) session = flowResponse.session
)
)
}
}, callback)
} }
mTestHelper.doSync<Unit> { callback -> mTestHelper.doSync<Unit> { callback ->
bobSession.cryptoService().crossSigningService() bobSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth( .initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = bobSession.myUserId, user = bobSession.myUserId,
password = TestConstants.PASSWORD password = TestConstants.PASSWORD,
), callback) session = flowResponse.session
)
)
}
}, callback)
} }
val aliceVerificationService = aliceSession.cryptoService().verificationService() val aliceVerificationService = aliceSession.cryptoService().verificationService()

View file

@ -26,6 +26,8 @@ import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
internal class UrlsExtractorTest : InstrumentedTest { internal class UrlsExtractorTest : InstrumentedTest {
@ -36,6 +38,7 @@ internal class UrlsExtractorTest : InstrumentedTest {
fun wrongEventTypeTest() { fun wrongEventTypeTest() {
createEvent(body = "https://matrix.org") createEvent(body = "https://matrix.org")
.copy(type = EventType.STATE_ROOM_GUEST_ACCESS) .copy(type = EventType.STATE_ROOM_GUEST_ACCESS)
.toFakeTimelineEvent()
.let { urlsExtractor.extract(it) } .let { urlsExtractor.extract(it) }
.size shouldBeEqualTo 0 .size shouldBeEqualTo 0
} }
@ -43,6 +46,7 @@ internal class UrlsExtractorTest : InstrumentedTest {
@Test @Test
fun oneUrlTest() { fun oneUrlTest() {
createEvent(body = "https://matrix.org") createEvent(body = "https://matrix.org")
.toFakeTimelineEvent()
.let { urlsExtractor.extract(it) } .let { urlsExtractor.extract(it) }
.let { result -> .let { result ->
result.size shouldBeEqualTo 1 result.size shouldBeEqualTo 1
@ -53,6 +57,7 @@ internal class UrlsExtractorTest : InstrumentedTest {
@Test @Test
fun withoutProtocolTest() { fun withoutProtocolTest() {
createEvent(body = "www.matrix.org") createEvent(body = "www.matrix.org")
.toFakeTimelineEvent()
.let { urlsExtractor.extract(it) } .let { urlsExtractor.extract(it) }
.size shouldBeEqualTo 0 .size shouldBeEqualTo 0
} }
@ -60,6 +65,7 @@ internal class UrlsExtractorTest : InstrumentedTest {
@Test @Test
fun oneUrlWithParamTest() { fun oneUrlWithParamTest() {
createEvent(body = "https://matrix.org?foo=bar") createEvent(body = "https://matrix.org?foo=bar")
.toFakeTimelineEvent()
.let { urlsExtractor.extract(it) } .let { urlsExtractor.extract(it) }
.let { result -> .let { result ->
result.size shouldBeEqualTo 1 result.size shouldBeEqualTo 1
@ -70,6 +76,7 @@ internal class UrlsExtractorTest : InstrumentedTest {
@Test @Test
fun oneUrlWithParamsTest() { fun oneUrlWithParamsTest() {
createEvent(body = "https://matrix.org?foo=bar&bar=foo") createEvent(body = "https://matrix.org?foo=bar&bar=foo")
.toFakeTimelineEvent()
.let { urlsExtractor.extract(it) } .let { urlsExtractor.extract(it) }
.let { result -> .let { result ->
result.size shouldBeEqualTo 1 result.size shouldBeEqualTo 1
@ -80,6 +87,7 @@ internal class UrlsExtractorTest : InstrumentedTest {
@Test @Test
fun oneUrlInlinedTest() { fun oneUrlInlinedTest() {
createEvent(body = "Hello https://matrix.org, how are you?") createEvent(body = "Hello https://matrix.org, how are you?")
.toFakeTimelineEvent()
.let { urlsExtractor.extract(it) } .let { urlsExtractor.extract(it) }
.let { result -> .let { result ->
result.size shouldBeEqualTo 1 result.size shouldBeEqualTo 1
@ -90,6 +98,7 @@ internal class UrlsExtractorTest : InstrumentedTest {
@Test @Test
fun twoUrlsTest() { fun twoUrlsTest() {
createEvent(body = "https://matrix.org https://example.org") createEvent(body = "https://matrix.org https://example.org")
.toFakeTimelineEvent()
.let { urlsExtractor.extract(it) } .let { urlsExtractor.extract(it) }
.let { result -> .let { result ->
result.size shouldBeEqualTo 2 result.size shouldBeEqualTo 2
@ -99,10 +108,26 @@ internal class UrlsExtractorTest : InstrumentedTest {
} }
private fun createEvent(body: String): Event = Event( private fun createEvent(body: String): Event = Event(
eventId = "!fake",
type = EventType.MESSAGE, type = EventType.MESSAGE,
content = MessageTextContent( content = MessageTextContent(
msgType = MessageType.MSGTYPE_TEXT, msgType = MessageType.MSGTYPE_TEXT,
body = body body = body
).toContent() ).toContent()
) )
private fun Event.toFakeTimelineEvent(): TimelineEvent {
return TimelineEvent(
root = this,
localId = 0L,
eventId = eventId!!,
displayIndex = 0,
senderInfo = SenderInfo(
userId = "",
displayName = null,
isUniqueDisplayName = true,
avatarUrl = null
)
)
}
} }

View file

@ -66,8 +66,8 @@ class TimelineForwardPaginationTest : InstrumentedTest {
numberOfMessagesToSend) numberOfMessagesToSend)
// Alice clear the cache // Alice clear the cache
commonTestHelper.doSync<Unit> { commonTestHelper.runBlockingTest {
aliceSession.clearCache(it) aliceSession.clearCache()
} }
// And restarts the sync // And restarts the sync

View file

@ -0,0 +1,69 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.auth
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
/**
* This class provides the authentication data by using user and password
*/
@JsonClass(generateAdapter = true)
data class TokenBasedAuth(
/**
* This is a session identifier that the client must pass back to the homeserver,
* if one is provided, in subsequent attempts to authenticate in the same API call.
*/
@Json(name = "session")
override val session: String? = null,
/**
* A client may receive a login token via some external service, such as email or SMS.
* Note that a login token is separate from an access token, the latter providing general authentication to various API endpoints.
*/
@Json(name = "token")
val token: String? = null,
/**
* The txn_id should be a random string generated by the client for the request.
* The same txn_id should be used if retrying the request.
* The txn_id may be used by the server to disallow other devices from using the token,
* thus providing "single use" tokens while still allowing the device to retry the request.
* This would be done by tying the token to the txn_id server side, as well as potentially invalidating
* the token completely once the device has successfully logged in
* (e.g. when we receive a request from the newly provisioned access_token).
*/
@Json(name = "txn_id")
val transactionId: String? = null,
// registration information
@Json(name = "type")
val type: String? = LoginFlowTypes.TOKEN
) : UIABaseAuth {
override fun hasAuthInfo() = token != null
override fun copyWithSession(session: String) = this.copy(session = session)
override fun asMap(): Map<String, *> = mapOf(
"session" to session,
"token" to token,
"transactionId" to transactionId,
"type" to type
)
}

View file

@ -0,0 +1,31 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.auth
interface UIABaseAuth {
/**
* This is a session identifier that the client must pass back to the homeserver,
* if one is provided, in subsequent attempts to authenticate in the same API call.
*/
val session: String?
fun hasAuthInfo(): Boolean
fun copyWithSession(session: String): UIABaseAuth
fun asMap() : Map<String, *>
}

View file

@ -0,0 +1,47 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.auth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import kotlin.coroutines.Continuation
/**
* Some API endpoints require authentication that interacts with the user.
* The homeserver may provide many different ways of authenticating, such as user/password auth, login via a social network (OAuth2),
* login by confirming a token sent to their email address, etc.
*
* The process takes the form of one or more 'stages'.
* At each stage the client submits a set of data for a given authentication type and awaits a response from the server,
* which will either be a final success or a request to perform an additional stage.
* This exchange continues until the final success.
*
* For each endpoint, a server offers one or more 'flows' that the client can use to authenticate itself.
* Each flow comprises a series of stages, as described above.
* The client is free to choose which flow it follows, however the flow's stages must be completed in order.
* Failing to follow the flows in order must result in an HTTP 401 response.
* When all stages in a flow are complete, authentication is complete and the API call succeeds.
*/
interface UserInteractiveAuthInterceptor {
/**
* When the API needs additional auth, this will be called.
* Implementation should check the flows from flow response and act accordingly.
* Updated auth should be provided using promise.resume, this allow implementation to perform
* an async operation (prompt for user password, open sso fallback) and then resume initial API call when done.
*/
fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>)
}

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.matrix.android.sdk.internal.crypto.model.rest package org.matrix.android.sdk.api.auth
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@ -27,7 +27,7 @@ data class UserPasswordAuth(
// device device session id // device device session id
@Json(name = "session") @Json(name = "session")
val session: String? = null, override val session: String? = null,
// registration information // registration information
@Json(name = "type") @Json(name = "type")
@ -38,4 +38,16 @@ data class UserPasswordAuth(
@Json(name = "password") @Json(name = "password")
val password: String? = null val password: String? = null
) ) : UIABaseAuth {
override fun hasAuthInfo() = password != null
override fun copyWithSession(session: String) = this.copy(session = session)
override fun asMap(): Map<String, *> = mapOf(
"session" to session,
"user" to user,
"password" to password,
"type" to type
)
}

View file

@ -38,15 +38,24 @@ data class SsoIdentityProvider(
* If present then it must be an HTTPS URL to an image resource. * If present then it must be an HTTPS URL to an image resource.
* This should be hosted by the homeserver service provider to not leak the client's IP address unnecessarily. * This should be hosted by the homeserver service provider to not leak the client's IP address unnecessarily.
*/ */
@Json(name = "icon") val iconUrl: String? @Json(name = "icon") val iconUrl: String?,
/**
* The `brand` field is **optional**. It allows the client to style the login
* button to suit a particular brand. It should be a string matching the
* "Common namespaced identifier grammar" as defined in
* [MSC2758](https://github.com/matrix-org/matrix-doc/pull/2758).
*/
@Json(name = "brand") val brand: String?
) : Parcelable { ) : Parcelable {
companion object { companion object {
// Not really defined by the spec, but we may define some ids here const val BRAND_GOOGLE = "org.matrix.google"
const val ID_GOOGLE = "google" const val BRAND_GITHUB = "org.matrix.github"
const val ID_GITHUB = "github" const val BRAND_APPLE = "org.matrix.apple"
const val ID_APPLE = "apple" const val BRAND_FACEBOOK = "org.matrix.facebook"
const val ID_FACEBOOK = "facebook" const val BRAND_TWITTER = "org.matrix.twitter"
const val ID_TWITTER = "twitter" const val BRAND_GITLAB = "org.matrix.gitlab"
} }
} }

View file

@ -14,14 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
package org.matrix.android.sdk.internal.auth.registration package org.matrix.android.sdk.api.auth.registration
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.registration.FlowResult
import org.matrix.android.sdk.api.auth.registration.Stage
import org.matrix.android.sdk.api.auth.registration.TermPolicies
import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.auth.data.InteractiveAuthenticationFlow import org.matrix.android.sdk.internal.auth.data.InteractiveAuthenticationFlow
@ -109,3 +106,8 @@ fun RegistrationFlowResponse.toFlowResult(): FlowResult {
return FlowResult(missingStage, completedStage) return FlowResult(missingStage, completedStage)
} }
fun RegistrationFlowResponse.nextUncompletedStage(flowIndex: Int = 0): String? {
val completed = completedStages ?: emptyList()
return flows?.getOrNull(flowIndex)?.stages?.firstOrNull { completed.contains(it).not() }
}

View file

@ -16,8 +16,8 @@
package org.matrix.android.sdk.api.failure package org.matrix.android.sdk.api.failure
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
import java.io.IOException import java.io.IOException
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
@ -43,6 +43,12 @@ fun Throwable.isInvalidPassword(): Boolean {
&& error.message == "Invalid password" && error.message == "Invalid password"
} }
fun Throwable.isInvalidUIAAuth(): Boolean {
return this is Failure.ServerError
&& error.code == MatrixError.M_FORBIDDEN
&& error.flows != null
}
/** /**
* Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible * Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible
*/ */
@ -53,6 +59,16 @@ fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? {
.adapter(RegistrationFlowResponse::class.java) .adapter(RegistrationFlowResponse::class.java)
.fromJson(this.errorBody) .fromJson(this.errorBody)
} }
} else if (this is Failure.ServerError && this.httpCode == 401 && this.error.code == MatrixError.M_FORBIDDEN) {
// This happens when the submission for this stage was bad (like bad password)
if (this.error.session != null && this.error.flows != null) {
RegistrationFlowResponse(
flows = this.error.flows,
session = this.error.session,
completedStages = this.error.completedStages,
params = this.error.params
)
} else null
} else { } else {
null null
} }

View file

@ -16,8 +16,8 @@
package org.matrix.android.sdk.api.failure package org.matrix.android.sdk.api.failure
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.internal.network.ssl.Fingerprint import org.matrix.android.sdk.internal.network.ssl.Fingerprint
import java.io.IOException import java.io.IOException

View file

@ -18,6 +18,8 @@ package org.matrix.android.sdk.api.failure
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.auth.data.InteractiveAuthenticationFlow
/** /**
* This data class holds the error defined by the matrix specifications. * This data class holds the error defined by the matrix specifications.
@ -42,7 +44,17 @@ data class MatrixError(
@Json(name = "soft_logout") val isSoftLogout: Boolean = false, @Json(name = "soft_logout") val isSoftLogout: Boolean = false,
// For M_INVALID_PEPPER // For M_INVALID_PEPPER
// {"error": "pepper does not match 'erZvr'", "lookup_pepper": "pQgMS", "algorithm": "sha256", "errcode": "M_INVALID_PEPPER"} // {"error": "pepper does not match 'erZvr'", "lookup_pepper": "pQgMS", "algorithm": "sha256", "errcode": "M_INVALID_PEPPER"}
@Json(name = "lookup_pepper") val newLookupPepper: String? = null @Json(name = "lookup_pepper") val newLookupPepper: String? = null,
// For M_FORBIDDEN UIA
@Json(name = "session")
val session: String? = null,
@Json(name = "completed")
val completedStages: List<String>? = null,
@Json(name = "flows")
val flows: List<InteractiveAuthenticationFlow>? = null,
@Json(name = "params")
val params: JsonDict? = null
) { ) {
companion object { companion object {

View file

@ -245,6 +245,8 @@ interface Session :
val sharedSecretStorageService: SharedSecretStorageService val sharedSecretStorageService: SharedSecretStorageService
fun getUiaSsoFallbackUrl(authenticationSessionId: String): String
/** /**
* Maintenance API, allows to print outs info on DB size to logcat * Maintenance API, allows to print outs info on DB size to logcat
*/ */

View file

@ -16,6 +16,8 @@
package org.matrix.android.sdk.api.session.account package org.matrix.android.sdk.api.session.account
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
/** /**
* This interface defines methods to manage the account. It's implemented at the session level. * This interface defines methods to manage the account. It's implemented at the session level.
*/ */
@ -43,5 +45,5 @@ interface AccountService {
* @param eraseAllData set to true to forget all messages that have been sent. Warning: this will cause future users to see * @param eraseAllData set to true to forget all messages that have been sent. Warning: this will cause future users to see
* an incomplete view of conversations * an incomplete view of conversations
*/ */
suspend fun deactivateAccount(password: String, eraseAllData: Boolean) suspend fun deactivateAccount(userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, eraseAllData: Boolean)
} }

View file

@ -16,8 +16,6 @@
package org.matrix.android.sdk.api.session.cache package org.matrix.android.sdk.api.session.cache
import org.matrix.android.sdk.api.MatrixCallback
/** /**
* This interface defines a method to clear the cache. It's implemented at the session level. * This interface defines a method to clear the cache. It's implemented at the session level.
*/ */
@ -26,5 +24,5 @@ interface CacheService {
/** /**
* Clear the whole cached data, except credentials. Once done, the sync has to be restarted by the sdk user. * Clear the whole cached data, except credentials. Once done, the sync has to be restarted by the sdk user.
*/ */
fun clearCache(callback: MatrixCallback<Unit>) suspend fun clearCache()
} }

View file

@ -20,6 +20,7 @@ import android.content.Context
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.paging.PagedList import androidx.paging.PagedList
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.listeners.ProgressListener
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
@ -53,7 +54,7 @@ interface CryptoService {
fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>)
fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>)
fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>) fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>)

View file

@ -18,10 +18,10 @@ package org.matrix.android.sdk.api.session.crypto.crosssigning
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustResult import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustResult
import org.matrix.android.sdk.internal.crypto.crosssigning.UserTrustResult import org.matrix.android.sdk.internal.crypto.crosssigning.UserTrustResult
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
interface CrossSigningService { interface CrossSigningService {
@ -40,7 +40,7 @@ interface CrossSigningService {
* Initialize cross signing for this user. * Initialize cross signing for this user.
* Users needs to enter credentials * Users needs to enter credentials
*/ */
fun initializeCrossSigning(authParams: UserPasswordAuth?, fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?,
callback: MatrixCallback<Unit>) callback: MatrixCallback<Unit>)
fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null

View file

@ -17,15 +17,16 @@
package org.matrix.android.sdk.api.session.media package org.matrix.android.sdk.api.session.media
import org.matrix.android.sdk.api.cache.CacheStrategy import org.matrix.android.sdk.api.cache.CacheStrategy
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.JsonDict
interface MediaService { interface MediaService {
/** /**
* Extract URLs from an Event. * Extract URLs from a TimelineEvent.
* @return the list of URLs contains in the body of the Event. It does not mean that URLs in this list have UrlPreview data * @param event TimelineEvent to extract the URL from.
* @return the list of URLs contains in the body of the TimelineEvent. It does not mean that URLs in this list have UrlPreview data
*/ */
fun extractUrls(event: Event): List<String> fun extractUrls(event: TimelineEvent): List<String>
/** /**
* Get Raw Url Preview data from the homeserver. There is no cache management for this request * Get Raw Url Preview data from the homeserver. There is no cache management for this request

View file

@ -20,6 +20,7 @@ package org.matrix.android.sdk.api.session.profile
import android.net.Uri import android.net.Uri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.JsonDict
@ -107,8 +108,7 @@ interface ProfileService {
* Finalize adding a 3Pids. Call this method once the user has validated that he owns the ThreePid * Finalize adding a 3Pids. Call this method once the user has validated that he owns the ThreePid
*/ */
fun finalizeAddingThreePid(threePid: ThreePid, fun finalizeAddingThreePid(threePid: ThreePid,
uiaSession: String?, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor,
accountPassword: String?,
matrixCallback: MatrixCallback<Unit>): Cancelable matrixCallback: MatrixCallback<Unit>): Cancelable
/** /**

View file

@ -89,6 +89,17 @@ data class TimelineEvent(
*/ */
fun TimelineEvent.hasBeenEdited() = annotations?.editSummary != null fun TimelineEvent.hasBeenEdited() = annotations?.editSummary != null
/**
* Get the latest known eventId for an edited event, or the eventId for an Event which has not been edited
*/
fun TimelineEvent.getLatestEventId(): String {
return annotations
?.editSummary
?.sourceEvents
?.lastOrNull()
?: eventId
}
/** /**
* Get the relation content if any * Get the relation content if any
*/ */

View file

@ -16,9 +16,7 @@
package org.matrix.android.sdk.api.session.signout package org.matrix.android.sdk.api.session.signout
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.util.Cancelable
/** /**
* This interface defines a method to sign out, or to renew the token. It's implemented at the session level. * This interface defines a method to sign out, or to renew the token. It's implemented at the session level.
@ -29,19 +27,16 @@ interface SignOutService {
* Ask the homeserver for a new access token. * Ask the homeserver for a new access token.
* The same deviceId will be used * The same deviceId will be used
*/ */
fun signInAgain(password: String, suspend fun signInAgain(password: String)
callback: MatrixCallback<Unit>): Cancelable
/** /**
* Update the session with credentials received after SSO * Update the session with credentials received after SSO
*/ */
fun updateCredentials(credentials: Credentials, suspend fun updateCredentials(credentials: Credentials)
callback: MatrixCallback<Unit>): Cancelable
/** /**
* Sign out, and release the session, clear all the session data, including crypto data * Sign out, and release the session, clear all the session data, including crypto data
* @param signOutFromHomeserver true if the sign out request has to be done * @param signOutFromHomeserver true if the sign out request has to be done
*/ */
fun signOut(signOutFromHomeserver: Boolean, suspend fun signOut(signOutFromHomeserver: Boolean)
callback: MatrixCallback<Unit>): Cancelable
} }

View file

@ -36,3 +36,6 @@ internal const val SSO_REDIRECT_PATH = "/_matrix/client/r0/login/sso/redirect"
internal const val MSC2858_SSO_REDIRECT_PATH = "/_matrix/client/unstable/org.matrix.msc2858/login/sso/redirect" internal const val MSC2858_SSO_REDIRECT_PATH = "/_matrix/client/unstable/org.matrix.msc2858/login/sso/redirect"
internal const val SSO_REDIRECT_URL_PARAM = "redirectUrl" internal const val SSO_REDIRECT_URL_PARAM = "redirectUrl"
// Ref: https://matrix.org/docs/spec/client_server/r0.6.1#single-sign-on
internal const val SSO_UIA_FALLBACK_PATH = "/_matrix/client/r0/auth/m.login.sso/fallback/web"

View file

@ -43,5 +43,6 @@ internal data class LoginFlow(
* See MSC #2858 * See MSC #2858
*/ */
@Json(name = "org.matrix.msc2858.identity_providers") @Json(name = "org.matrix.msc2858.identity_providers")
val ssoIdentityProvider: List<SsoIdentityProvider>? val ssoIdentityProvider: List<SsoIdentityProvider>? = null
) )

View file

@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
import org.matrix.android.sdk.api.auth.registration.RegistrationResult import org.matrix.android.sdk.api.auth.registration.RegistrationResult
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
import org.matrix.android.sdk.api.auth.registration.toFlowResult
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.Failure.RegistrationFlowError import org.matrix.android.sdk.api.failure.Failure.RegistrationFlowError
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable

View file

@ -0,0 +1,53 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.auth.registration
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
import org.matrix.android.sdk.api.auth.UIABaseAuth
import timber.log.Timber
import kotlin.coroutines.suspendCoroutine
internal suspend fun handleUIA(failure: Throwable, interceptor: UserInteractiveAuthInterceptor, retryBlock: suspend (UIABaseAuth) -> Unit): Boolean {
Timber.d("## UIA: check error ${failure.message}")
val flowResponse = failure.toRegistrationFlowResponse()
?: return false.also {
Timber.d("## UIA: not a UIA error")
}
Timber.d("## UIA: error can be passed to interceptor")
Timber.d("## UIA: type = ${flowResponse.flows}")
Timber.d("## UIA: delegate to interceptor...")
val authUpdate = try {
suspendCoroutine<UIABaseAuth> { continuation ->
interceptor.performStage(flowResponse, (failure as? Failure.ServerError)?.error?.code, continuation)
}
} catch (failure: Throwable) {
Timber.w(failure, "## UIA: failed to participate")
return false
}
Timber.d("## UIA: updated auth $authUpdate")
return try {
retryBlock(authUpdate)
true
} catch (failure: Throwable) {
handleUIA(failure, interceptor, retryBlock)
}
}

View file

@ -30,6 +30,7 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.NoOpMatrixCallback
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.crypto.MXCryptoConfig import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
@ -207,9 +208,9 @@ internal class DefaultCryptoService @Inject constructor(
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) { override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>) {
deleteDeviceTask deleteDeviceTask
.configureWith(DeleteDeviceTask.Params(deviceId)) { .configureWith(DeleteDeviceTask.Params(deviceId, userInteractiveAuthInterceptor, null)) {
this.executionThread = TaskThread.CRYPTO this.executionThread = TaskThread.CRYPTO
this.callback = callback this.callback = callback
} }

View file

@ -19,30 +19,30 @@ package org.matrix.android.sdk.internal.crypto.crosssigning
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.work.BackoffPolicy import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask
import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.TaskThread import org.matrix.android.sdk.internal.task.TaskThread
import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.olm.OlmPkSigning import org.matrix.olm.OlmPkSigning
import org.matrix.olm.OlmUtility import org.matrix.olm.OlmUtility
@ -61,7 +61,10 @@ internal class DefaultCrossSigningService @Inject constructor(
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope, private val cryptoCoroutineScope: CoroutineScope,
private val workManagerProvider: WorkManagerProvider) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener { private val workManagerProvider: WorkManagerProvider,
private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
) : CrossSigningService,
DeviceListManager.UserDevicesUpdateListener {
private var olmUtility: OlmUtility? = null private var olmUtility: OlmUtility? = null
@ -147,11 +150,11 @@ internal class DefaultCrossSigningService @Inject constructor(
* - Sign the keys and upload them * - Sign the keys and upload them
* - Sign the current device with SSK and sign MSK with device key (migration) and upload signatures * - Sign the current device with SSK and sign MSK with device key (migration) and upload signatures
*/ */
override fun initializeCrossSigning(authParams: UserPasswordAuth?, callback: MatrixCallback<Unit>) { override fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?, callback: MatrixCallback<Unit>) {
Timber.d("## CrossSigning initializeCrossSigning") Timber.d("## CrossSigning initializeCrossSigning")
val params = InitializeCrossSigningTask.Params( val params = InitializeCrossSigningTask.Params(
authParams = authParams interactiveAuthInterceptor = uiaInterceptor
) )
initializeCrossSigningTask.configureWith(params) { initializeCrossSigningTask.configureWith(params) {
this.callbackThread = TaskThread.CRYPTO this.callbackThread = TaskThread.CRYPTO
@ -689,7 +692,7 @@ internal class DefaultCrossSigningService @Inject constructor(
return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted)) return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted))
} }
fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo) : DeviceTrustResult { fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo): DeviceTrustResult {
val locallyTrusted = otherDevice.trustLevel?.isLocallyVerified() val locallyTrusted = otherDevice.trustLevel?.isLocallyVerified()
myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId)) myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId))
@ -747,8 +750,11 @@ internal class DefaultCrossSigningService @Inject constructor(
} }
override fun onUsersDeviceUpdate(userIds: List<String>) { override fun onUsersDeviceUpdate(userIds: List<String>) {
Timber.d("## CrossSigning - onUsersDeviceUpdate for $userIds") Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users: $userIds")
val workerParams = UpdateTrustWorker.Params(sessionId = sessionId, updatedUserIds = userIds) val workerParams = UpdateTrustWorker.Params(
sessionId = sessionId,
filename = updateTrustWorkerDataRepository.createParam(userIds)
)
val workerData = WorkerParamsFactory.toData(workerParams) val workerData = WorkerParamsFactory.toData(workerParams)
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<UpdateTrustWorker>() val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<UpdateTrustWorker>()

View file

@ -55,7 +55,11 @@ internal class UpdateTrustWorker(context: Context,
internal data class Params( internal data class Params(
override val sessionId: String, override val sessionId: String,
override val lastFailureMessage: String? = null, override val lastFailureMessage: String? = null,
val updatedUserIds: List<String> // Kept for compatibility, but not used anymore (can be used for pending Worker)
val updatedUserIds: List<String>? = null,
// Passing a long list of userId can break the Work Manager due to data size limitation.
// so now we use a temporary file to store the data
val filename: String? = null
) : SessionWorkerParams ) : SessionWorkerParams
@Inject lateinit var crossSigningService: DefaultCrossSigningService @Inject lateinit var crossSigningService: DefaultCrossSigningService
@ -64,6 +68,7 @@ internal class UpdateTrustWorker(context: Context,
@CryptoDatabase @Inject lateinit var realmConfiguration: RealmConfiguration @CryptoDatabase @Inject lateinit var realmConfiguration: RealmConfiguration
@UserId @Inject lateinit var myUserId: String @UserId @Inject lateinit var myUserId: String
@Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper @Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper
@Inject lateinit var updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
@SessionDatabase @Inject lateinit var sessionRealmConfiguration: RealmConfiguration @SessionDatabase @Inject lateinit var sessionRealmConfiguration: RealmConfiguration
// @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater // @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater
@ -74,7 +79,17 @@ internal class UpdateTrustWorker(context: Context,
} }
override suspend fun doSafeWork(params: Params): Result { override suspend fun doSafeWork(params: Params): Result {
var userList = params.updatedUserIds var userList = params.filename
?.let { updateTrustWorkerDataRepository.getParam(it) }
?.userIds
?: params.updatedUserIds.orEmpty()
if (userList.isEmpty()) {
// This should not happen, but let's avoid go further in case of empty list
cleanup(params)
return Result.success()
}
// Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user, // Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user,
// or a new device?) So we check all again :/ // or a new device?) So we check all again :/
@ -213,9 +228,15 @@ internal class UpdateTrustWorker(context: Context,
} }
} }
cleanup(params)
return Result.success() return Result.success()
} }
private fun cleanup(params: Params) {
params.filename
?.let { updateTrustWorkerDataRepository.delete(it) }
}
private fun updateCrossSigningKeysTrust(realm: Realm, userId: String, verified: Boolean) { private fun updateCrossSigningKeysTrust(realm: Realm, userId: String, verified: Boolean) {
val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java) val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId) .equalTo(CrossSigningInfoEntityFields.USER_ID, userId)

View file

@ -0,0 +1,65 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.crosssigning
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.di.SessionFilesDirectory
import java.io.File
import java.util.UUID
import javax.inject.Inject
@JsonClass(generateAdapter = true)
internal data class UpdateTrustWorkerData(
@Json(name = "userIds")
val userIds: List<String>
)
internal class UpdateTrustWorkerDataRepository @Inject constructor(
@SessionFilesDirectory parentDir: File
) {
private val workingDirectory = File(parentDir, "tw")
private val jsonAdapter = MoshiProvider.providesMoshi().adapter(UpdateTrustWorkerData::class.java)
// Return the path of the created file
fun createParam(userIds: List<String>): String {
val filename = "${UUID.randomUUID()}.json"
workingDirectory.mkdirs()
val file = File(workingDirectory, filename)
UpdateTrustWorkerData(userIds = userIds)
.let { jsonAdapter.toJson(it) }
.let { file.writeText(it) }
return filename
}
fun getParam(filename: String): UpdateTrustWorkerData? {
return File(workingDirectory, filename)
.takeIf { it.exists() }
?.readText()
?.let { jsonAdapter.fromJson(it) }
}
fun delete(filename: String) {
tryOrNull("Unable to delete $filename") {
File(workingDirectory, filename).delete()
}
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.model.rest
import org.matrix.android.sdk.api.auth.UIABaseAuth
data class DefaultBaseAuth(
/**
* This is a session identifier that the client must pass back to the homeserver,
* if one is provided, in subsequent attempts to authenticate in the same API call.
*/
override val session: String? = null
) : UIABaseAuth {
override fun hasAuthInfo() = true
override fun copyWithSession(session: String) = this.copy(session = session)
override fun asMap(): Map<String, *> = mapOf("session" to session)
}

View file

@ -24,5 +24,5 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class DeleteDeviceParams( internal data class DeleteDeviceParams(
@Json(name = "auth") @Json(name = "auth")
val userPasswordAuth: UserPasswordAuth? = null val auth: Map<String, *>? = null
) )

View file

@ -30,5 +30,5 @@ internal data class UploadSigningKeysBody(
val userSigningKey: RestKeyInfo? = null, val userSigningKey: RestKeyInfo? = null,
@Json(name = "auth") @Json(name = "auth")
val auth: UserPasswordAuth? = null val auth: Map<String, *>? = null
) )

View file

@ -16,18 +16,22 @@
package org.matrix.android.sdk.internal.crypto.tasks package org.matrix.android.sdk.internal.crypto.tasks
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse import org.matrix.android.sdk.internal.auth.registration.handleUIA
import org.matrix.android.sdk.internal.crypto.api.CryptoApi import org.matrix.android.sdk.internal.crypto.api.CryptoApi
import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> { internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> {
data class Params( data class Params(
val deviceId: String val deviceId: String,
val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?,
val userAuthParam: UIABaseAuth?
) )
} }
@ -39,12 +43,17 @@ internal class DefaultDeleteDeviceTask @Inject constructor(
override suspend fun execute(params: DeleteDeviceTask.Params) { override suspend fun execute(params: DeleteDeviceTask.Params) {
try { try {
executeRequest<Unit>(globalErrorReceiver) { executeRequest<Unit>(globalErrorReceiver) {
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams()) apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams(params.userAuthParam?.asMap()))
} }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
throw throwable.toRegistrationFlowResponse() if (params.userInteractiveAuthInterceptor == null
?.let { Failure.RegistrationFlowError(it) } || !handleUIA(throwable, params.userInteractiveAuthInterceptor) { auth ->
?: throwable execute(params.copy(userAuthParam = auth))
}
) {
Timber.d("## UIA: propagate failure")
throw throwable
}
} }
} }
} }

View file

@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.crypto.tasks
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.internal.crypto.api.CryptoApi import org.matrix.android.sdk.internal.crypto.api.CryptoApi
import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
@ -44,12 +44,12 @@ internal class DefaultDeleteDeviceWithUserPasswordTask @Inject constructor(
return executeRequest(globalErrorReceiver) { return executeRequest(globalErrorReceiver) {
apiCall = cryptoApi.deleteDevice(params.deviceId, apiCall = cryptoApi.deleteDevice(params.deviceId,
DeleteDeviceParams( DeleteDeviceParams(
userPasswordAuth = UserPasswordAuth( auth = UserPasswordAuth(
type = LoginFlowTypes.PASSWORD, type = LoginFlowTypes.PASSWORD,
session = params.authSession, session = params.authSession,
user = userId, user = userId,
password = params.password password = params.password
) ).asMap()
) )
) )
} }

View file

@ -17,6 +17,8 @@
package org.matrix.android.sdk.internal.crypto.tasks package org.matrix.android.sdk.internal.crypto.tasks
import dagger.Lazy import dagger.Lazy
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.internal.auth.registration.handleUIA
import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder
import org.matrix.android.sdk.internal.crypto.crosssigning.canonicalSignable import org.matrix.android.sdk.internal.crypto.crosssigning.canonicalSignable
@ -24,7 +26,6 @@ import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
import org.matrix.android.sdk.internal.crypto.model.KeyUsage import org.matrix.android.sdk.internal.crypto.model.KeyUsage
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.android.sdk.internal.util.JsonCanonicalizer
@ -34,7 +35,7 @@ import javax.inject.Inject
internal interface InitializeCrossSigningTask : Task<InitializeCrossSigningTask.Params, InitializeCrossSigningTask.Result> { internal interface InitializeCrossSigningTask : Task<InitializeCrossSigningTask.Params, InitializeCrossSigningTask.Result> {
data class Params( data class Params(
val authParams: UserPasswordAuth? val interactiveAuthInterceptor: UserInteractiveAuthInterceptor?
) )
data class Result( data class Result(
@ -117,10 +118,21 @@ internal class DefaultInitializeCrossSigningTask @Inject constructor(
.key(sskPublicKey) .key(sskPublicKey)
.signature(userId, masterPublicKey, signedSSK) .signature(userId, masterPublicKey, signedSSK)
.build(), .build(),
userPasswordAuth = params.authParams userAuthParam = null
// userAuthParam = params.authParams
) )
try {
uploadSigningKeysTask.execute(uploadSigningKeysParams) uploadSigningKeysTask.execute(uploadSigningKeysParams)
} catch (failure: Throwable) {
if (params.interactiveAuthInterceptor == null
|| !handleUIA(failure, params.interactiveAuthInterceptor) { authUpdate ->
uploadSigningKeysTask.execute(uploadSigningKeysParams.copy(userAuthParam = authUpdate))
}) {
Timber.d("## UIA: propagate failure")
throw failure
}
}
// Sign the current device with SSK // Sign the current device with SSK
val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder() val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder()

View file

@ -16,14 +16,12 @@
package org.matrix.android.sdk.internal.crypto.tasks package org.matrix.android.sdk.internal.crypto.tasks
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
import org.matrix.android.sdk.internal.crypto.api.CryptoApi import org.matrix.android.sdk.internal.crypto.api.CryptoApi
import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import org.matrix.android.sdk.internal.crypto.model.toRest import org.matrix.android.sdk.internal.crypto.model.toRest
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
@ -39,15 +37,9 @@ internal interface UploadSigningKeysTask : Task<UploadSigningKeysTask.Params, Un
// the SSK // the SSK
val selfSignedKey: CryptoCrossSigningKey, val selfSignedKey: CryptoCrossSigningKey,
/** /**
* - If null: * Authorisation info (User Interactive flow)
* - no retry will be performed
* - If not null, it may or may not contain a sessionId:
* - If sessionId is null:
* - password should not be null: the task will perform a first request to get a sessionId, and then a second one
* - If sessionId is not null:
* - password should not be null as well, and no retry will be performed
*/ */
val userPasswordAuth: UserPasswordAuth? val userAuthParam: UIABaseAuth?
) )
} }
@ -59,31 +51,13 @@ internal class DefaultUploadSigningKeysTask @Inject constructor(
) : UploadSigningKeysTask { ) : UploadSigningKeysTask {
override suspend fun execute(params: UploadSigningKeysTask.Params) { override suspend fun execute(params: UploadSigningKeysTask.Params) {
val paramsHaveSessionId = params.userPasswordAuth?.session != null
val uploadQuery = UploadSigningKeysBody( val uploadQuery = UploadSigningKeysBody(
masterKey = params.masterKey.toRest(), masterKey = params.masterKey.toRest(),
userSigningKey = params.userKey.toRest(), userSigningKey = params.userKey.toRest(),
selfSigningKey = params.selfSignedKey.toRest(), selfSigningKey = params.selfSignedKey.toRest(),
// If sessionId is provided, use the userPasswordAuth auth = params.userAuthParam?.asMap()
auth = params.userPasswordAuth.takeIf { paramsHaveSessionId }
) )
try {
doRequest(uploadQuery) doRequest(uploadQuery)
} catch (throwable: Throwable) {
val registrationFlowResponse = throwable.toRegistrationFlowResponse()
if (registrationFlowResponse != null
&& registrationFlowResponse.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true }
&& params.userPasswordAuth?.password != null
&& !paramsHaveSessionId
) {
// Retry with authentication
doRequest(uploadQuery.copy(auth = params.userPasswordAuth.copy(session = registrationFlowResponse.session)))
} else {
// Other error
throw throwable
}
}
} }
private suspend fun doRequest(uploadQuery: UploadSigningKeysBody) { private suspend fun doRequest(uploadQuery: UploadSigningKeysBody) {

View file

@ -20,7 +20,6 @@ import androidx.annotation.MainThread
import dagger.Lazy import dagger.Lazy
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.auth.data.SessionParams
import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.failure.GlobalError
import org.matrix.android.sdk.api.pushrules.PushRuleService import org.matrix.android.sdk.api.pushrules.PushRuleService
@ -53,6 +52,8 @@ import org.matrix.android.sdk.api.session.terms.TermsService
import org.matrix.android.sdk.api.session.typing.TypingUsersTracker import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
import org.matrix.android.sdk.api.session.user.UserService import org.matrix.android.sdk.api.session.user.UserService
import org.matrix.android.sdk.api.session.widgets.WidgetService import org.matrix.android.sdk.api.session.widgets.WidgetService
import org.matrix.android.sdk.api.util.appendParamToUrl
import org.matrix.android.sdk.internal.auth.SSO_UIA_FALLBACK_PATH
import org.matrix.android.sdk.internal.auth.SessionParamsStore import org.matrix.android.sdk.internal.auth.SessionParamsStore
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
import org.matrix.android.sdk.internal.database.tools.RealmDebugTools import org.matrix.android.sdk.internal.database.tools.RealmDebugTools
@ -217,13 +218,13 @@ internal class DefaultSession @Inject constructor(
} }
} }
override fun clearCache(callback: MatrixCallback<Unit>) { override suspend fun clearCache() {
stopSync() stopSync()
stopAnyBackgroundSync() stopAnyBackgroundSync()
uiHandler.post { uiHandler.post {
lifecycleObservers.forEach { it.onClearCache() } lifecycleObservers.forEach { it.onClearCache() }
} }
cacheService.get().clearCache(callback) cacheService.get().clearCache()
workManagerProvider.cancelAllWorks() workManagerProvider.cancelAllWorks()
} }
@ -274,6 +275,18 @@ internal class DefaultSession @Inject constructor(
return "$myUserId - ${sessionParams.deviceId}" return "$myUserId - ${sessionParams.deviceId}"
} }
override fun getUiaSsoFallbackUrl(authenticationSessionId: String): String {
val hsBas = sessionParams.homeServerConnectionConfig
.homeServerUri
.toString()
.trim { it == '/' }
return buildString {
append(hsBas)
append(SSO_UIA_FALLBACK_PATH)
appendParamToUrl("session", authenticationSessionId)
}
}
override fun logDbUsageInfo() { override fun logDbUsageInfo() {
RealmDebugTools(realmConfiguration).logInfo("Session") RealmDebugTools(realmConfiguration).logInfo("Session")
} }

View file

@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.session.account
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth import org.matrix.android.sdk.api.auth.UserPasswordAuth
/** /**
* Class to pass request parameters to update the password. * Class to pass request parameters to update the password.

View file

@ -18,21 +18,21 @@ package org.matrix.android.sdk.internal.session.account
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth import org.matrix.android.sdk.api.auth.UIABaseAuth
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class DeactivateAccountParams( internal data class DeactivateAccountParams(
@Json(name = "auth")
val auth: UserPasswordAuth? = null,
// Set to true to erase all data of the account // Set to true to erase all data of the account
@Json(name = "erase") @Json(name = "erase")
val erase: Boolean val erase: Boolean,
@Json(name = "auth")
val auth: Map<String, *>? = null
) { ) {
companion object { companion object {
fun create(userId: String, password: String, erase: Boolean): DeactivateAccountParams { fun create(auth: UIABaseAuth?, erase: Boolean): DeactivateAccountParams {
return DeactivateAccountParams( return DeactivateAccountParams(
auth = UserPasswordAuth(user = userId, password = password), auth = auth?.asMap(),
erase = erase erase = erase
) )
} }

View file

@ -16,6 +16,9 @@
package org.matrix.android.sdk.internal.session.account package org.matrix.android.sdk.internal.session.account
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.internal.auth.registration.handleUIA
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
@ -27,8 +30,9 @@ import javax.inject.Inject
internal interface DeactivateAccountTask : Task<DeactivateAccountTask.Params, Unit> { internal interface DeactivateAccountTask : Task<DeactivateAccountTask.Params, Unit> {
data class Params( data class Params(
val password: String, val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor,
val eraseAllData: Boolean val eraseAllData: Boolean,
val userAuthParam: UIABaseAuth? = null
) )
} }
@ -41,12 +45,21 @@ internal class DefaultDeactivateAccountTask @Inject constructor(
) : DeactivateAccountTask { ) : DeactivateAccountTask {
override suspend fun execute(params: DeactivateAccountTask.Params) { override suspend fun execute(params: DeactivateAccountTask.Params) {
val deactivateAccountParams = DeactivateAccountParams.create(userId, params.password, params.eraseAllData) val deactivateAccountParams = DeactivateAccountParams.create(params.userAuthParam, params.eraseAllData)
try {
executeRequest<Unit>(globalErrorReceiver) { executeRequest<Unit>(globalErrorReceiver) {
apiCall = accountAPI.deactivate(deactivateAccountParams) apiCall = accountAPI.deactivate(deactivateAccountParams)
} }
} catch (throwable: Throwable) {
if (!handleUIA(throwable, params.userInteractiveAuthInterceptor) { auth ->
execute(params.copy(userAuthParam = auth))
}
) {
Timber.d("## UIA: propagate failure")
throw throwable
}
}
// Logout from identity server if any, ignoring errors // Logout from identity server if any, ignoring errors
runCatching { identityDisconnectTask.execute(Unit) } runCatching { identityDisconnectTask.execute(Unit) }
.onFailure { Timber.w(it, "Unable to disconnect identity server") } .onFailure { Timber.w(it, "Unable to disconnect identity server") }

View file

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.session.account package org.matrix.android.sdk.internal.session.account
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.session.account.AccountService import org.matrix.android.sdk.api.session.account.AccountService
import javax.inject.Inject import javax.inject.Inject
@ -26,7 +27,7 @@ internal class DefaultAccountService @Inject constructor(private val changePassw
changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword)) changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword))
} }
override suspend fun deactivateAccount(password: String, eraseAllData: Boolean) { override suspend fun deactivateAccount(userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, eraseAllData: Boolean) {
deactivateAccountTask.execute(DeactivateAccountTask.Params(password, eraseAllData)) deactivateAccountTask.execute(DeactivateAccountTask.Params(userInteractiveAuthInterceptor, eraseAllData))
} }
} }

View file

@ -16,23 +16,18 @@
package org.matrix.android.sdk.internal.session.cache package org.matrix.android.sdk.internal.session.cache
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.cache.CacheService import org.matrix.android.sdk.api.session.cache.CacheService
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
import javax.inject.Inject import javax.inject.Inject
internal class DefaultCacheService @Inject constructor(@SessionDatabase internal class DefaultCacheService @Inject constructor(@SessionDatabase
private val clearCacheTask: ClearCacheTask, private val clearCacheTask: ClearCacheTask,
private val taskExecutor: TaskExecutor) : CacheService { private val taskExecutor: TaskExecutor
) : CacheService {
override fun clearCache(callback: MatrixCallback<Unit>) { override suspend fun clearCache() {
taskExecutor.cancelAll() taskExecutor.cancelAll()
clearCacheTask clearCacheTask.execute(Unit)
.configureWith {
this.callback = callback
}
.executeBy(taskExecutor)
} }
} }

View file

@ -28,9 +28,8 @@ import java.io.File
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
internal class ImageCompressor @Inject constructor() { internal class ImageCompressor @Inject constructor(private val context: Context) {
suspend fun compress( suspend fun compress(
context: Context,
imageFile: File, imageFile: File,
desiredWidth: Int, desiredWidth: Int,
desiredHeight: Int, desiredHeight: Int,
@ -46,7 +45,7 @@ internal class ImageCompressor @Inject constructor() {
} }
} ?: return@withContext imageFile } ?: return@withContext imageFile
val destinationFile = createDestinationFile(context) val destinationFile = createDestinationFile()
runCatching { runCatching {
destinationFile.outputStream().use { destinationFile.outputStream().use {
@ -118,7 +117,7 @@ internal class ImageCompressor @Inject constructor() {
} }
} }
private fun createDestinationFile(context: Context): File { private fun createDestinationFile(): File {
return File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) return File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
} }
} }

View file

@ -47,8 +47,7 @@ internal object ThumbnailExtractor {
val mediaMetadataRetriever = MediaMetadataRetriever() val mediaMetadataRetriever = MediaMetadataRetriever()
try { try {
mediaMetadataRetriever.setDataSource(context, attachment.queryUri) mediaMetadataRetriever.setDataSource(context, attachment.queryUri)
val thumbnail = mediaMetadataRetriever.frameAtTime mediaMetadataRetriever.frameAtTime?.let { thumbnail ->
val outputStream = ByteArrayOutputStream() val outputStream = ByteArrayOutputStream()
thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
val thumbnailWidth = thumbnail.width val thumbnailWidth = thumbnail.width
@ -63,6 +62,9 @@ internal object ThumbnailExtractor {
) )
thumbnail.recycle() thumbnail.recycle()
outputStream.reset() outputStream.reset()
} ?: run {
Timber.e("Cannot extract video thumbnail at %s", attachment.queryUri.toString())
}
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Cannot extract video thumbnail") Timber.e(e, "Cannot extract video thumbnail")
} finally { } finally {

View file

@ -156,7 +156,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
// Do not compress gif // Do not compress gif
&& attachment.mimeType != MimeTypes.Gif && attachment.mimeType != MimeTypes.Gif
&& params.compressBeforeSending) { && params.compressBeforeSending) {
fileToUpload = imageCompressor.compress(context, workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE) fileToUpload = imageCompressor.compress(workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE)
.also { compressedFile -> .also { compressedFile ->
// Get new Bitmap size // Get new Bitmap size
compressedFile.inputStream().use { compressedFile.inputStream().use {

View file

@ -52,65 +52,60 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor(
val pepper = identityData.hashLookupPepper val pepper = identityData.hashLookupPepper
val hashDetailResponse = if (pepper == null) { val hashDetailResponse = if (pepper == null) {
// We need to fetch the hash details first // We need to fetch the hash details first
fetchAndStoreHashDetails(identityAPI) fetchHashDetails(identityAPI)
.also { identityStore.setHashDetails(it) }
} else { } else {
IdentityHashDetailResponse(pepper, identityData.hashLookupAlgorithm) IdentityHashDetailResponse(pepper, identityData.hashLookupAlgorithm)
} }
if (hashDetailResponse.algorithms.contains("sha256").not()) { if (hashDetailResponse.algorithms.contains(IdentityHashDetailResponse.ALGORITHM_SHA256).not()) {
// TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment we do not do it // TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment we do not do it
// Also, what we have in cache could be outdated, the identity server maybe now supports sha256 // Also, what we have in cache could be outdated, the identity server maybe now supports sha256
throw IdentityServiceError.BulkLookupSha256NotSupported throw IdentityServiceError.BulkLookupSha256NotSupported
} }
val hashedAddresses = withOlmUtility { olmUtility -> val lookUpData = lookUpInternal(identityAPI, params.threePids, hashDetailResponse, true)
params.threePids.map { threePid ->
base64ToBase64Url(
olmUtility.sha256(threePid.value.toLowerCase(Locale.ROOT)
+ " " + threePid.toMedium() + " " + hashDetailResponse.pepper)
)
}
}
val identityLookUpV2Response = lookUpInternal(identityAPI, hashedAddresses, hashDetailResponse, true)
// Convert back to List<FoundThreePid> // Convert back to List<FoundThreePid>
return handleSuccess(params.threePids, hashedAddresses, identityLookUpV2Response) return handleSuccess(params.threePids, lookUpData)
} }
data class LookUpData(
val hashedAddresses: List<String>,
val identityLookUpResponse: IdentityLookUpResponse
)
private suspend fun lookUpInternal(identityAPI: IdentityAPI, private suspend fun lookUpInternal(identityAPI: IdentityAPI,
hashedAddresses: List<String>, threePids: List<ThreePid>,
hashDetailResponse: IdentityHashDetailResponse, hashDetailResponse: IdentityHashDetailResponse,
canRetry: Boolean): IdentityLookUpResponse { canRetry: Boolean): LookUpData {
val hashedAddresses = getHashedAddresses(threePids, hashDetailResponse.pepper)
return try { return try {
LookUpData(hashedAddresses,
executeRequest(null) { executeRequest(null) {
apiCall = identityAPI.lookup(IdentityLookUpParams( apiCall = identityAPI.lookup(IdentityLookUpParams(
hashedAddresses, hashedAddresses,
IdentityHashDetailResponse.ALGORITHM_SHA256, IdentityHashDetailResponse.ALGORITHM_SHA256,
hashDetailResponse.pepper hashDetailResponse.pepper
)) ))
} })
} catch (failure: Throwable) { } catch (failure: Throwable) {
// Catch invalid hash pepper and retry // Catch invalid hash pepper and retry
if (canRetry && failure is Failure.ServerError && failure.error.code == MatrixError.M_INVALID_PEPPER) { if (canRetry && failure is Failure.ServerError && failure.error.code == MatrixError.M_INVALID_PEPPER) {
// This is not documented, but the error can contain the new pepper! // This is not documented, but the error can contain the new pepper!
if (!failure.error.newLookupPepper.isNullOrEmpty()) { val newHashDetailResponse = if (!failure.error.newLookupPepper.isNullOrEmpty()) {
// Store it and use it right now // Store it and use it right now
hashDetailResponse.copy(pepper = failure.error.newLookupPepper) hashDetailResponse.copy(pepper = failure.error.newLookupPepper)
.also { identityStore.setHashDetails(it) }
.let { lookUpInternal(identityAPI, hashedAddresses, it, false /* Avoid infinite loop */) }
} else { } else {
// Retrieve the new hash details // Retrieve the new hash details
val newHashDetailResponse = fetchAndStoreHashDetails(identityAPI) fetchHashDetails(identityAPI)
}
if (hashDetailResponse.algorithms.contains(IdentityHashDetailResponse.ALGORITHM_SHA256).not()) { .also { identityStore.setHashDetails(it) }
if (newHashDetailResponse.algorithms.contains(IdentityHashDetailResponse.ALGORITHM_SHA256).not()) {
// TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment we do not do it // TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment we do not do it
// Also, what we have in cache is maybe outdated, the identity server maybe now support sha256
throw IdentityServiceError.BulkLookupSha256NotSupported throw IdentityServiceError.BulkLookupSha256NotSupported
} }
lookUpInternal(identityAPI, threePids, newHashDetailResponse, false /* Avoid infinite loop */)
lookUpInternal(identityAPI, hashedAddresses, newHashDetailResponse, false /* Avoid infinite loop */)
}
} else { } else {
// Other error // Other error
throw failure throw failure
@ -118,16 +113,29 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor(
} }
} }
private suspend fun fetchAndStoreHashDetails(identityAPI: IdentityAPI): IdentityHashDetailResponse { private fun getHashedAddresses(threePids: List<ThreePid>, pepper: String): List<String> {
return executeRequest<IdentityHashDetailResponse>(null) { return withOlmUtility { olmUtility ->
apiCall = identityAPI.hashDetails() threePids.map { threePid ->
base64ToBase64Url(
olmUtility.sha256(threePid.value.toLowerCase(Locale.ROOT)
+ " " + threePid.toMedium() + " " + pepper)
)
}
} }
.also { identityStore.setHashDetails(it) }
} }
private fun handleSuccess(threePids: List<ThreePid>, hashedAddresses: List<String>, identityLookUpResponse: IdentityLookUpResponse): List<FoundThreePid> { private suspend fun fetchHashDetails(identityAPI: IdentityAPI): IdentityHashDetailResponse {
return identityLookUpResponse.mappings.keys.map { hashedAddress -> return executeRequest(null) {
FoundThreePid(threePids[hashedAddresses.indexOf(hashedAddress)], identityLookUpResponse.mappings[hashedAddress] ?: error("")) apiCall = identityAPI.hashDetails()
}
}
private fun handleSuccess(threePids: List<ThreePid>, lookupData: LookUpData): List<FoundThreePid> {
return lookupData.identityLookUpResponse.mappings.keys.map { hashedAddress ->
FoundThreePid(
threePids[lookupData.hashedAddresses.indexOf(hashedAddress)],
lookupData.identityLookUpResponse.mappings[hashedAddress] ?: error("")
)
} }
} }
} }

View file

@ -18,9 +18,10 @@ package org.matrix.android.sdk.internal.session.media
import androidx.collection.LruCache import androidx.collection.LruCache
import org.matrix.android.sdk.api.cache.CacheStrategy import org.matrix.android.sdk.api.cache.CacheStrategy
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.media.MediaService import org.matrix.android.sdk.api.session.media.MediaService
import org.matrix.android.sdk.api.session.media.PreviewUrlData import org.matrix.android.sdk.api.session.media.PreviewUrlData
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLatestEventId
import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.util.getOrPut import org.matrix.android.sdk.internal.util.getOrPut
import javax.inject.Inject import javax.inject.Inject
@ -34,11 +35,12 @@ internal class DefaultMediaService @Inject constructor(
// Cache of extracted URLs // Cache of extracted URLs
private val extractedUrlsCache = LruCache<String, List<String>>(1_000) private val extractedUrlsCache = LruCache<String, List<String>>(1_000)
override fun extractUrls(event: Event): List<String> { override fun extractUrls(event: TimelineEvent): List<String> {
return extractedUrlsCache.getOrPut(event.cacheKey()) { urlsExtractor.extract(event) } return extractedUrlsCache.getOrPut(event.cacheKey()) { urlsExtractor.extract(event) }
} }
private fun Event.cacheKey() = "${eventId ?: ""}-${roomId ?: ""}" // Use the id of the latest Event edition
private fun TimelineEvent.cacheKey() = "${getLatestEventId()}-${root.roomId ?: ""}"
override suspend fun getRawPreviewUrl(url: String, timestamp: Long?): JsonDict { override suspend fun getRawPreviewUrl(url: String, timestamp: Long?): JsonDict {
return getRawPreviewUrlTask.execute(GetRawPreviewUrlTask.Params(url, timestamp)) return getRawPreviewUrlTask.execute(GetRawPreviewUrlTask.Params(url, timestamp))

View file

@ -17,21 +17,19 @@
package org.matrix.android.sdk.internal.session.media package org.matrix.android.sdk.internal.session.media
import android.util.Patterns import android.util.Patterns
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import javax.inject.Inject import javax.inject.Inject
internal class UrlsExtractor @Inject constructor() { internal class UrlsExtractor @Inject constructor() {
// Sadly Patterns.WEB_URL_WITH_PROTOCOL is not public so filter the protocol later // Sadly Patterns.WEB_URL_WITH_PROTOCOL is not public so filter the protocol later
private val urlRegex = Patterns.WEB_URL.toRegex() private val urlRegex = Patterns.WEB_URL.toRegex()
fun extract(event: Event): List<String> { fun extract(event: TimelineEvent): List<String> {
return event.takeIf { it.getClearType() == EventType.MESSAGE } return event.takeIf { it.root.getClearType() == EventType.MESSAGE }
?.getClearContent() ?.getLastMessageContent()
?.toModel<MessageContent>()
?.takeIf { ?.takeIf {
it.msgType == MessageType.MSGTYPE_TEXT it.msgType == MessageType.MSGTYPE_TEXT
|| it.msgType == MessageType.MSGTYPE_NOTICE || it.msgType == MessageType.MSGTYPE_NOTICE

View file

@ -22,6 +22,7 @@ import androidx.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.kotlin.where import io.realm.kotlin.where
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.profile.ProfileService
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
@ -170,14 +171,12 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
} }
override fun finalizeAddingThreePid(threePid: ThreePid, override fun finalizeAddingThreePid(threePid: ThreePid,
uiaSession: String?, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor,
accountPassword: String?,
matrixCallback: MatrixCallback<Unit>): Cancelable { matrixCallback: MatrixCallback<Unit>): Cancelable {
return finalizeAddingThreePidTask return finalizeAddingThreePidTask
.configureWith(FinalizeAddingThreePidTask.Params( .configureWith(FinalizeAddingThreePidTask.Params(
threePid = threePid, threePid = threePid,
session = uiaSession, userInteractiveAuthInterceptor = userInteractiveAuthInterceptor,
accountPassword = accountPassword,
userWantsToCancel = false userWantsToCancel = false
)) { )) {
callback = alsoRefresh(matrixCallback) callback = alsoRefresh(matrixCallback)
@ -189,8 +188,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
return finalizeAddingThreePidTask return finalizeAddingThreePidTask
.configureWith(FinalizeAddingThreePidTask.Params( .configureWith(FinalizeAddingThreePidTask.Params(
threePid = threePid, threePid = threePid,
session = null, userInteractiveAuthInterceptor = null,
accountPassword = null,
userWantsToCancel = true userWantsToCancel = true
)) { )) {
callback = alsoRefresh(matrixCallback) callback = alsoRefresh(matrixCallback)

View file

@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.session.profile
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class FinalizeAddThreePidBody( internal data class FinalizeAddThreePidBody(
@ -37,5 +36,5 @@ internal data class FinalizeAddThreePidBody(
* Additional authentication information for the user-interactive authentication API. * Additional authentication information for the user-interactive authentication API.
*/ */
@Json(name = "auth") @Json(name = "auth")
val auth: UserPasswordAuth? val auth: Map<String, *>? = null
) )

View file

@ -17,10 +17,12 @@
package org.matrix.android.sdk.internal.session.profile package org.matrix.android.sdk.internal.session.profile
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth import org.matrix.android.sdk.internal.auth.registration.handleUIA
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
@ -29,13 +31,14 @@ import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.awaitTransaction
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal abstract class FinalizeAddingThreePidTask : Task<FinalizeAddingThreePidTask.Params, Unit> { internal abstract class FinalizeAddingThreePidTask : Task<FinalizeAddingThreePidTask.Params, Unit> {
data class Params( data class Params(
val threePid: ThreePid, val threePid: ThreePid,
val session: String?, val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?,
val accountPassword: String?, val userAuthParam: UIABaseAuth? = null,
val userWantsToCancel: Boolean val userWantsToCancel: Boolean
) )
} }
@ -62,22 +65,23 @@ internal class DefaultFinalizeAddingThreePidTask @Inject constructor(
val body = FinalizeAddThreePidBody( val body = FinalizeAddThreePidBody(
clientSecret = pendingThreePids.clientSecret, clientSecret = pendingThreePids.clientSecret,
sid = pendingThreePids.sid, sid = pendingThreePids.sid,
auth = if (params.session != null && params.accountPassword != null) { auth = params.userAuthParam?.asMap()
UserPasswordAuth(
session = params.session,
user = userId,
password = params.accountPassword
)
} else null
) )
apiCall = profileAPI.finalizeAddThreePid(body) apiCall = profileAPI.finalizeAddThreePid(body)
} }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
if (params.userInteractiveAuthInterceptor == null
|| !handleUIA(throwable, params.userInteractiveAuthInterceptor) { auth ->
execute(params.copy(userAuthParam = auth))
}
) {
Timber.d("## UIA: propagate failure")
throw throwable.toRegistrationFlowResponse() throw throwable.toRegistrationFlowResponse()
?.let { Failure.RegistrationFlowError(it) } ?.let { Failure.RegistrationFlowError(it) }
?: throwable ?: throwable
} }
} }
}
cleanupDatabase(params) cleanupDatabase(params)
} }

View file

@ -143,9 +143,11 @@ internal class CreateRoomBodyBuilder @Inject constructor(
} }
private suspend fun canEnableEncryption(params: CreateRoomParams): Boolean { private suspend fun canEnableEncryption(params: CreateRoomParams): Boolean {
return (params.enableEncryptionIfInvitedUsersSupportIt return params.enableEncryptionIfInvitedUsersSupportIt
&& crossSigningService.isCrossSigningVerified() // Parity with web, enable if users have encryption ready devices
&& params.invite3pids.isEmpty()) // for now remove checks on cross signing and 3pid invites
// && crossSigningService.isCrossSigningVerified()
&& params.invite3pids.isEmpty()
&& params.invitedUserIds.isNotEmpty() && params.invitedUserIds.isNotEmpty()
&& params.invitedUserIds.let { userIds -> && params.invitedUserIds.let { userIds ->
val keys = deviceListManager.downloadKeys(userIds, forceDownload = false) val keys = deviceListManager.downloadKeys(userIds, forceDownload = false)

View file

@ -141,7 +141,7 @@ internal class DefaultRelationService @AssistedInject constructor(
} }
override fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) { override fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) {
val params = FetchEditHistoryTask.Params(roomId, cryptoSessionInfoProvider.isRoomEncrypted(roomId), eventId) val params = FetchEditHistoryTask.Params(roomId, eventId)
fetchEditHistoryTask fetchEditHistoryTask
.configureWith(params) { .configureWith(params) {
this.callback = callback this.callback = callback

View file

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.relation
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.RoomAPI
@ -25,25 +26,27 @@ import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject import javax.inject.Inject
internal interface FetchEditHistoryTask : Task<FetchEditHistoryTask.Params, List<Event>> { internal interface FetchEditHistoryTask : Task<FetchEditHistoryTask.Params, List<Event>> {
data class Params( data class Params(
val roomId: String, val roomId: String,
val isRoomEncrypted: Boolean,
val eventId: String val eventId: String
) )
} }
internal class DefaultFetchEditHistoryTask @Inject constructor( internal class DefaultFetchEditHistoryTask @Inject constructor(
private val roomAPI: RoomAPI, private val roomAPI: RoomAPI,
private val globalErrorReceiver: GlobalErrorReceiver private val globalErrorReceiver: GlobalErrorReceiver,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider
) : FetchEditHistoryTask { ) : FetchEditHistoryTask {
override suspend fun execute(params: FetchEditHistoryTask.Params): List<Event> { override suspend fun execute(params: FetchEditHistoryTask.Params): List<Event> {
val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
val response = executeRequest<RelationsResponse>(globalErrorReceiver) { val response = executeRequest<RelationsResponse>(globalErrorReceiver) {
apiCall = roomAPI.getRelations(params.roomId, apiCall = roomAPI.getRelations(
params.eventId, roomId = params.roomId,
RelationType.REPLACE, eventId = params.eventId,
if (params.isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE) relationType = RelationType.REPLACE,
eventType = if (isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE
)
} }
val events = response.chunks.toMutableList() val events = response.chunks.toMutableList()

View file

@ -140,14 +140,13 @@ internal class RoomSummaryUpdater @Inject constructor(
.queryActiveRoomMembersEvent() .queryActiveRoomMembersEvent()
.notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) .notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId)
.findAll() .findAll()
.asSequence()
.map { it.userId } .map { it.userId }
roomSummaryEntity.otherMemberIds.clear() roomSummaryEntity.otherMemberIds.clear()
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
if (roomSummaryEntity.isEncrypted) { if (roomSummaryEntity.isEncrypted) {
// mmm maybe we could only refresh shield instead of checking trust also? // mmm maybe we could only refresh shield instead of checking trust also?
crossSigningService.onUsersDeviceUpdate(roomSummaryEntity.otherMemberIds.toList()) crossSigningService.onUsersDeviceUpdate(otherRoomMembers)
} }
} }
} }

View file

@ -21,16 +21,34 @@ import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class EventContextResponse( internal data class EventContextResponse(
/**
* Details of the requested event.
*/
@Json(name = "event") val event: Event, @Json(name = "event") val event: Event,
/**
* A token that can be used to paginate backwards with.
*/
@Json(name = "start") override val start: String? = null, @Json(name = "start") override val start: String? = null,
@Json(name = "events_before") val eventsBefore: List<Event> = emptyList(), /**
@Json(name = "events_after") val eventsAfter: List<Event> = emptyList(), * A list of room events that happened just before the requested event, in reverse-chronological order.
*/
@Json(name = "events_before") val eventsBefore: List<Event>? = null,
/**
* A list of room events that happened just after the requested event, in chronological order.
*/
@Json(name = "events_after") val eventsAfter: List<Event>? = null,
/**
* A token that can be used to paginate forwards with.
*/
@Json(name = "end") override val end: String? = null, @Json(name = "end") override val end: String? = null,
@Json(name = "state") override val stateEvents: List<Event> = emptyList() /**
* The state of the room at the last event returned.
*/
@Json(name = "state") override val stateEvents: List<Event>? = null
) : TokenChunkEvent { ) : TokenChunkEvent {
override val events: List<Event> by lazy { override val events: List<Event> by lazy {
eventsAfter.reversed() + listOf(event) + eventsBefore eventsAfter.orEmpty().reversed() + event + eventsBefore.orEmpty()
} }
} }

View file

@ -22,8 +22,28 @@ import org.matrix.android.sdk.api.session.events.model.Event
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class PaginationResponse( internal data class PaginationResponse(
/**
* The token the pagination starts from. If dir=b this will be the token supplied in from.
*/
@Json(name = "start") override val start: String? = null, @Json(name = "start") override val start: String? = null,
/**
* The token the pagination ends at. If dir=b this token should be used again to request even earlier events.
*/
@Json(name = "end") override val end: String? = null, @Json(name = "end") override val end: String? = null,
@Json(name = "chunk") override val events: List<Event> = emptyList(), /**
@Json(name = "state") override val stateEvents: List<Event> = emptyList() * A list of room events. The order depends on the dir parameter. For dir=b events will be in
) : TokenChunkEvent * reverse-chronological order, for dir=f in chronological order, so that events start at the from point.
*/
@Json(name = "chunk") val chunk: List<Event>? = null,
/**
* A list of state events relevant to showing the chunk. For example, if lazy_load_members is enabled
* in the filter then this may contain the membership events for the senders of events in the chunk.
*
* Unless include_redundant_members is true, the server may remove membership events which would have
* already been sent to the client in prior calls to this endpoint, assuming the membership of those members has not changed.
*/
@Json(name = "state") override val stateEvents: List<Event>? = null
) : TokenChunkEvent {
override val events: List<Event>
get() = chunk.orEmpty()
}

View file

@ -22,7 +22,7 @@ internal interface TokenChunkEvent {
val start: String? val start: String?
val end: String? val end: String?
val events: List<Event> val events: List<Event>
val stateEvents: List<Event> val stateEvents: List<Event>?
fun hasMore() = start != end fun hasMore() = start != end
} }

View file

@ -156,7 +156,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
} }
} }
return if (receivedChunk.events.isEmpty()) { return if (receivedChunk.events.isEmpty()) {
if (receivedChunk.start != receivedChunk.end) { if (receivedChunk.hasMore()) {
Result.SHOULD_FETCH_MORE Result.SHOULD_FETCH_MORE
} else { } else {
Result.REACHED_END Result.REACHED_END
@ -196,7 +196,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
for (stateEvent in stateEvents) { stateEvents?.forEach { stateEvent ->
val ageLocalTs = stateEvent.unsignedData?.age?.let { now - it } val ageLocalTs = stateEvent.unsignedData?.age?.let { now - it }
val stateEventEntity = stateEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) val stateEventEntity = stateEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
currentChunk.addStateEvent(roomId, stateEventEntity, direction) currentChunk.addStateEvent(roomId, stateEventEntity, direction)
@ -205,9 +205,9 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
} }
} }
val eventIds = ArrayList<String>(eventList.size) val eventIds = ArrayList<String>(eventList.size)
for (event in eventList) { eventList.forEach { event ->
if (event.eventId == null || event.senderId == null) { if (event.eventId == null || event.senderId == null) {
continue return@forEach
} }
val ageLocalTs = event.unsignedData?.age?.let { now - it } val ageLocalTs = event.unsignedData?.age?.let { now - it }
eventIds.add(event.eventId) eventIds.add(event.eventId)

View file

@ -56,8 +56,8 @@ internal class DefaultGetUploadsTask @Inject constructor(
private val roomAPI: RoomAPI, private val roomAPI: RoomAPI,
private val tokenStore: SyncTokenStore, private val tokenStore: SyncTokenStore,
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val globalErrorReceiver: GlobalErrorReceiver) private val globalErrorReceiver: GlobalErrorReceiver
: GetUploadsTask { ) : GetUploadsTask {
override suspend fun execute(params: GetUploadsTask.Params): GetUploadsResult { override suspend fun execute(params: GetUploadsTask.Params): GetUploadsResult {
val result: GetUploadsResult val result: GetUploadsResult

View file

@ -16,45 +16,25 @@
package org.matrix.android.sdk.internal.session.signout package org.matrix.android.sdk.internal.session.signout
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.session.signout.SignOutService import org.matrix.android.sdk.api.session.signout.SignOutService
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.internal.auth.SessionParamsStore import org.matrix.android.sdk.internal.auth.SessionParamsStore
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.task.launchToCallback
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import javax.inject.Inject import javax.inject.Inject
internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask, internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask,
private val signInAgainTask: SignInAgainTask, private val signInAgainTask: SignInAgainTask,
private val sessionParamsStore: SessionParamsStore, private val sessionParamsStore: SessionParamsStore
private val coroutineDispatchers: MatrixCoroutineDispatchers, ) : SignOutService {
private val taskExecutor: TaskExecutor) : SignOutService {
override fun signInAgain(password: String, override suspend fun signInAgain(password: String) {
callback: MatrixCallback<Unit>): Cancelable { signInAgainTask.execute(SignInAgainTask.Params(password))
return signInAgainTask
.configureWith(SignInAgainTask.Params(password)) {
this.callback = callback
}
.executeBy(taskExecutor)
} }
override fun updateCredentials(credentials: Credentials, override suspend fun updateCredentials(credentials: Credentials) {
callback: MatrixCallback<Unit>): Cancelable {
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
sessionParamsStore.updateCredentials(credentials) sessionParamsStore.updateCredentials(credentials)
} }
}
override fun signOut(signOutFromHomeserver: Boolean, override suspend fun signOut(signOutFromHomeserver: Boolean) {
callback: MatrixCallback<Unit>): Cancelable { return signOutTask.execute(SignOutTask.Params(signOutFromHomeserver))
return signOutTask
.configureWith(SignOutTask.Params(signOutFromHomeserver)) {
this.callback = callback
}
.executeBy(taskExecutor)
} }
} }

View file

@ -53,7 +53,7 @@ internal class WidgetFactory @Inject constructor(private val userDataSource: Use
} }
} }
val isAddedByMe = widgetEvent.senderId == userId val isAddedByMe = widgetEvent.senderId == userId
val computedUrl = widgetContent.computeURL(widgetEvent.roomId) val computedUrl = widgetContent.computeURL(widgetEvent.roomId, widgetId)
return Widget( return Widget(
widgetContent = widgetContent, widgetContent = widgetContent,
event = widgetEvent, event = widgetEvent,
@ -65,13 +65,14 @@ internal class WidgetFactory @Inject constructor(private val userDataSource: Use
) )
} }
private fun WidgetContent.computeURL(roomId: String?): String? { private fun WidgetContent.computeURL(roomId: String?, widgetId: String): String? {
var computedUrl = url ?: return null var computedUrl = url ?: return null
val myUser = userDataSource.getUser(userId) val myUser = userDataSource.getUser(userId)
computedUrl = computedUrl computedUrl = computedUrl
.replace("\$matrix_user_id", userId) .replace("\$matrix_user_id", userId)
.replace("\$matrix_display_name", myUser?.displayName ?: userId) .replace("\$matrix_display_name", myUser?.displayName ?: userId)
.replace("\$matrix_avatar_url", myUser?.avatarUrl ?: "") .replace("\$matrix_avatar_url", myUser?.avatarUrl ?: "")
.replace("\$matrix_widget_id", widgetId)
if (roomId != null) { if (roomId != null) {
computedUrl = computedUrl.replace("\$matrix_room_id", roomId) computedUrl = computedUrl.replace("\$matrix_room_id", roomId)

View file

@ -19,11 +19,11 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-parcelize'
android { android {
compileSdkVersion 29 compileSdkVersion 30
defaultConfig { defaultConfig {
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 29 targetSdkVersion 30
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"

View file

@ -58,7 +58,7 @@ class AudioPicker : Picker<MultiPickerAudioType>() {
context.contentResolver.openFileDescriptor(selectedUri, "r")?.use { pfd -> context.contentResolver.openFileDescriptor(selectedUri, "r")?.use { pfd ->
val mediaMetadataRetriever = MediaMetadataRetriever() val mediaMetadataRetriever = MediaMetadataRetriever()
mediaMetadataRetriever.setDataSource(pfd.fileDescriptor) mediaMetadataRetriever.setDataSource(pfd.fileDescriptor)
duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION).toLong() duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L
} }
audioList.add( audioList.add(

View file

@ -61,10 +61,10 @@ class VideoPicker : Picker<MultiPickerVideoType>() {
context.contentResolver.openFileDescriptor(selectedUri, "r")?.use { pfd -> context.contentResolver.openFileDescriptor(selectedUri, "r")?.use { pfd ->
val mediaMetadataRetriever = MediaMetadataRetriever() val mediaMetadataRetriever = MediaMetadataRetriever()
mediaMetadataRetriever.setDataSource(pfd.fileDescriptor) mediaMetadataRetriever.setDataSource(pfd.fileDescriptor)
duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION).toLong() duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L
width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH).toInt() width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: 0
height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT).toInt() height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: 0
orientation = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION).toInt() orientation = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toInt() ?: 0
} }
videoList.add( videoList.add(

View file

@ -13,7 +13,7 @@ kapt {
// Note: 2 digits max for each value // Note: 2 digits max for each value
ext.versionMajor = 1 ext.versionMajor = 1
ext.versionMinor = 0 ext.versionMinor = 0
ext.versionPatch = 15 ext.versionPatch = 16
static def getGitTimestamp() { static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct' def cmd = 'git show -s --format=%ct'
@ -101,7 +101,7 @@ ext.abiVersionCodes = ["armeabi-v7a": 1, "arm64-v8a": 2, "x86": 3, "x86_64": 4].
def buildNumber = System.env.BUILDKITE_BUILD_NUMBER as Integer ?: 0 def buildNumber = System.env.BUILDKITE_BUILD_NUMBER as Integer ?: 0
android { android {
compileSdkVersion 29 compileSdkVersion 30
// Due to a bug introduced in Android gradle plugin 3.6.0, we have to specify the ndk version to use // 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 // Ref: https://issuetracker.google.com/issues/144111441
@ -111,7 +111,7 @@ android {
applicationId "im.vector.app" applicationId "im.vector.app"
// Set to API 21: see #405 // Set to API 21: see #405
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 30
multiDexEnabled true multiDexEnabled true
// `develop` branch will have version code from timestamp, to ensure each build from CI has a incremented versionCode. // `develop` branch will have version code from timestamp, to ensure each build from CI has a incremented versionCode.

View file

@ -42,13 +42,18 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
@ -67,10 +72,18 @@ class VerifySessionInteractiveTest : VerificationTestBase() {
existingSession = createAccountAndSync(matrix, userName, password, true) existingSession = createAccountAndSync(matrix, userName, password, true)
doSync<Unit> { doSync<Unit> {
existingSession!!.cryptoService().crossSigningService() existingSession!!.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth( .initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = existingSession!!.myUserId, user = existingSession!!.myUserId,
password = "password" password = "password",
), it) session = flowResponse.session
)
)
}
}, it)
} }
} }

View file

@ -46,8 +46,13 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
@ -67,17 +72,35 @@ class VerifySessionPassphraseTest : VerificationTestBase() {
existingSession = createAccountAndSync(matrix, userName, password, true) existingSession = createAccountAndSync(matrix, userName, password, true)
doSync<Unit> { doSync<Unit> {
existingSession!!.cryptoService().crossSigningService() existingSession!!.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth( .initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = existingSession!!.myUserId, user = existingSession!!.myUserId,
password = "password" password = "password",
), it) session = flowResponse.session
)
)
}
}, it)
} }
val task = BootstrapCrossSigningTask(existingSession!!, StringProvider(context.resources)) val task = BootstrapCrossSigningTask(existingSession!!, StringProvider(context.resources))
runBlocking { runBlocking {
task.execute(Params( task.execute(Params(
userPasswordAuth = UserPasswordAuth(password = password), userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = existingSession!!.myUserId,
password = password,
session = flowResponse.session
)
)
}
},
passphrase = passphrase, passphrase = passphrase,
setupMode = SetupMode.NORMAL setupMode = SetupMode.NORMAL
)) ))

View file

@ -196,6 +196,8 @@ class UiAllScreensSanityTest {
pressBack() pressBack()
clickMenu(R.id.video_call) clickMenu(R.id.video_call)
pressBack() pressBack()
clickMenu(R.id.search)
pressBack()
pressBack() pressBack()
} }

View file

@ -63,7 +63,6 @@
<activity <activity
android:name=".features.MainActivity" android:name=".features.MainActivity"
android:taskAffinity=""
android:theme="@style/AppTheme.Launcher" /> android:theme="@style/AppTheme.Launcher" />
<!-- Activity alias for the launcher Activity (must be declared after the Activity it targets) --> <!-- Activity alias for the launcher Activity (must be declared after the Activity it targets) -->
@ -242,6 +241,27 @@
<activity android:name=".features.home.room.detail.search.SearchActivity" /> <activity android:name=".features.home.room.detail.search.SearchActivity" />
<activity android:name=".features.usercode.UserCodeActivity" /> <activity android:name=".features.usercode.UserCodeActivity" />
<!-- Single instance is very important for the custom scheme callback-->
<activity android:name=".features.auth.ReAuthActivity"
android:launchMode="singleInstance"
android:exported="false">
<!-- XXX: UIA SSO has only web fallback, i.e no url redirect, so for now we comment this out
hopefully, we would use it when finally available
-->
<!-- Add intent filter to handle redirection URL after SSO login in external browser -->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.VIEW" />-->
<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
<!-- <data-->
<!-- android:host="reauth"-->
<!-- android:scheme="element" />-->
<!-- </intent-filter>-->
</activity>
<!-- Services --> <!-- Services -->
<service <service

View file

@ -28,7 +28,7 @@ import im.vector.app.features.crypto.keysbackup.settings.KeysBackupSettingsFragm
import im.vector.app.features.crypto.quads.SharedSecuredStorageKeyFragment import im.vector.app.features.crypto.quads.SharedSecuredStorageKeyFragment
import im.vector.app.features.crypto.quads.SharedSecuredStoragePassphraseFragment import im.vector.app.features.crypto.quads.SharedSecuredStoragePassphraseFragment
import im.vector.app.features.crypto.quads.SharedSecuredStorageResetAllFragment import im.vector.app.features.crypto.quads.SharedSecuredStorageResetAllFragment
import im.vector.app.features.crypto.recover.BootstrapAccountPasswordFragment import im.vector.app.features.crypto.recover.BootstrapReAuthFragment
import im.vector.app.features.crypto.recover.BootstrapConclusionFragment import im.vector.app.features.crypto.recover.BootstrapConclusionFragment
import im.vector.app.features.crypto.recover.BootstrapConfirmPassphraseFragment import im.vector.app.features.crypto.recover.BootstrapConfirmPassphraseFragment
import im.vector.app.features.crypto.recover.BootstrapEnterPassphraseFragment import im.vector.app.features.crypto.recover.BootstrapEnterPassphraseFragment
@ -522,8 +522,8 @@ interface FragmentModule {
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(BootstrapAccountPasswordFragment::class) @FragmentKey(BootstrapReAuthFragment::class)
fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment fun bindBootstrapReAuthFragment(fragment: BootstrapReAuthFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap

View file

@ -25,6 +25,7 @@ import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.preference.UserAvatarPreference import im.vector.app.core.preference.UserAvatarPreference
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
import im.vector.app.features.auth.ReAuthActivity
import im.vector.app.features.call.CallControlsBottomSheet import im.vector.app.features.call.CallControlsBottomSheet
import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.conference.VectorJitsiActivity import im.vector.app.features.call.conference.VectorJitsiActivity
@ -145,6 +146,7 @@ interface ScreenComponent {
fun inject(activity: VectorJitsiActivity) fun inject(activity: VectorJitsiActivity)
fun inject(activity: SearchActivity) fun inject(activity: SearchActivity)
fun inject(activity: UserCodeActivity) fun inject(activity: UserCodeActivity)
fun inject(activity: ReAuthActivity)
/* ========================================================================================== /* ==========================================================================================
* BottomSheets * BottomSheets

View file

@ -105,11 +105,13 @@ class DefaultErrorFormatter @Inject constructor(
HttpURLConnection.HTTP_NOT_FOUND -> HttpURLConnection.HTTP_NOT_FOUND ->
// homeserver not found // homeserver not found
stringProvider.getString(R.string.login_error_no_homeserver_found) stringProvider.getString(R.string.login_error_no_homeserver_found)
HttpURLConnection.HTTP_UNAUTHORIZED ->
// uia errors?
stringProvider.getString(R.string.error_unauthorized)
else -> else ->
throwable.localizedMessage throwable.localizedMessage
} }
} }
is SsoFlowNotSupportedYet -> stringProvider.getString(R.string.error_sso_flow_not_supported_yet)
else -> throwable.localizedMessage else -> throwable.localizedMessage
} }
?: stringProvider.getString(R.string.unknown_error) ?: stringProvider.getString(R.string.unknown_error)

View file

@ -19,10 +19,12 @@ package im.vector.app.core.platform
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.WindowInsetsController
import android.view.WindowManager import android.view.WindowManager
import android.widget.TextView import android.widget.TextView
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
@ -33,6 +35,7 @@ import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentFactory import androidx.fragment.app.FragmentFactory
@ -410,7 +413,18 @@ abstract class VectorBaseActivity<VB: ViewBinding> : AppCompatActivity(), HasScr
/** /**
* Force to render the activity in fullscreen * Force to render the activity in fullscreen
*/ */
@Suppress("DEPRECATION")
private fun setFullScreen() { private fun setFullScreen() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
window.setDecorFitsSystemWindows(false)
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
// New API instead of FLAG_TRANSLUCENT_STATUS
window.statusBarColor = ContextCompat.getColor(this, im.vector.lib.attachmentviewer.R.color.half_transparent_status_bar)
// New API instead of FLAG_TRANSLUCENT_NAVIGATION
window.navigationBarColor = ContextCompat.getColor(this, im.vector.lib.attachmentviewer.R.color.half_transparent_status_bar)
} else {
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
@ -418,6 +432,7 @@ abstract class VectorBaseActivity<VB: ViewBinding> : AppCompatActivity(), HasScr
or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
} }
}
/* ========================================================================================== /* ==========================================================================================
* MENU MANAGEMENT * MENU MANAGEMENT

View file

@ -200,6 +200,7 @@ abstract class VectorBaseFragment<VB: ViewBinding> : BaseMvRxFragment(), HasScre
} }
protected fun showLoadingDialog(message: CharSequence? = null, cancelable: Boolean = false) { protected fun showLoadingDialog(message: CharSequence? = null, cancelable: Boolean = false) {
progress?.dismiss()
progress = ProgressDialog(requireContext()).apply { progress = ProgressDialog(requireContext()).apply {
setCancelable(cancelable) setCancelable(cancelable)
setMessage(message ?: getString(R.string.please_wait)) setMessage(message ?: getString(R.string.please_wait))

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.ui.list
import android.view.View
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.google.android.material.button.MaterialButton
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
/**
* A generic button list item.
*/
@EpoxyModelClass(layout = R.layout.item_positive_button)
abstract class GenericPositiveButtonItem : VectorEpoxyModel<GenericPositiveButtonItem.Holder>() {
@EpoxyAttribute
var text: String? = null
@EpoxyAttribute
var buttonClickAction: View.OnClickListener? = null
@EpoxyAttribute
@ColorInt
var textColor: Int? = null
@EpoxyAttribute
@DrawableRes
var iconRes: Int? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.button.text = text
if (iconRes != null) {
holder.button.setIconResource(iconRes!!)
} else {
holder.button.icon = null
}
buttonClickAction?.let { holder.button.setOnClickListener(it) }
}
class Holder : VectorEpoxyHolder() {
val button by bind<MaterialButton>(R.id.itemGenericItemButton)
}
}

View file

@ -22,6 +22,7 @@ import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
@ -45,10 +46,8 @@ import im.vector.app.features.signout.soft.SoftLogoutActivity
import im.vector.app.features.ui.UiStateRepository import im.vector.app.features.ui.UiStateRepository
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.failure.GlobalError
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -147,38 +146,39 @@ class MainActivity : VectorBaseActivity<FragmentLoadingBinding>(), UnlockedActiv
} }
when { when {
args.isAccountDeactivated -> { args.isAccountDeactivated -> {
lifecycleScope.launch {
// Just do the local cleanup // Just do the local cleanup
Timber.w("Account deactivated, start app") Timber.w("Account deactivated, start app")
sessionHolder.clearActiveSession() sessionHolder.clearActiveSession()
doLocalCleanup(clearPreferences = true) doLocalCleanup(clearPreferences = true)
startNextActivityAndFinish() startNextActivityAndFinish()
} }
args.clearCredentials -> session.signOut( }
!args.isUserLoggedOut, args.clearCredentials -> {
object : MatrixCallback<Unit> { lifecycleScope.launch {
override fun onSuccess(data: Unit) { try {
session.signOut(!args.isUserLoggedOut)
Timber.w("SIGN_OUT: success, start app") Timber.w("SIGN_OUT: success, start app")
sessionHolder.clearActiveSession() sessionHolder.clearActiveSession()
doLocalCleanup(clearPreferences = true) doLocalCleanup(clearPreferences = true)
startNextActivityAndFinish() startNextActivityAndFinish()
} } catch (failure: Throwable) {
override fun onFailure(failure: Throwable) {
displayError(failure) displayError(failure)
} }
}) }
args.clearCache -> session.clearCache( }
object : MatrixCallback<Unit> { args.clearCache -> {
override fun onSuccess(data: Unit) { lifecycleScope.launch {
try {
session.clearCache()
doLocalCleanup(clearPreferences = false) doLocalCleanup(clearPreferences = false)
session.startSyncing(applicationContext) session.startSyncing(applicationContext)
startNextActivityAndFinish() startNextActivityAndFinish()
} } catch (failure: Throwable) {
override fun onFailure(failure: Throwable) {
displayError(failure) displayError(failure)
} }
}) }
}
} }
} }
@ -187,8 +187,7 @@ class MainActivity : VectorBaseActivity<FragmentLoadingBinding>(), UnlockedActiv
Timber.w("Ignoring invalid token global error") Timber.w("Ignoring invalid token global error")
} }
private fun doLocalCleanup(clearPreferences: Boolean) { private suspend fun doLocalCleanup(clearPreferences: Boolean) {
GlobalScope.launch(Dispatchers.Main) {
// On UI Thread // On UI Thread
Glide.get(this@MainActivity).clearMemory() Glide.get(this@MainActivity).clearMemory()
@ -206,7 +205,6 @@ class MainActivity : VectorBaseActivity<FragmentLoadingBinding>(), UnlockedActiv
deleteAllFiles(this@MainActivity.cacheDir) deleteAllFiles(this@MainActivity.cacheDir)
} }
} }
}
private fun displayError(failure: Throwable) { private fun displayError(failure: Throwable) {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {

View file

@ -19,6 +19,7 @@ package im.vector.app.features.attachments.preview
import android.app.Activity.RESULT_CANCELED import android.app.Activity.RESULT_CANCELED
import android.app.Activity.RESULT_OK import android.app.Activity.RESULT_OK
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
@ -153,8 +154,13 @@ class AttachmentsPreviewFragment @Inject constructor(
) )
} }
@Suppress("DEPRECATION")
private fun applyInsets() { private fun applyInsets() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
activity?.window?.setDecorFitsSystemWindows(false)
} else {
view?.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION view?.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
}
ViewCompat.setOnApplyWindowInsetsListener(views.attachmentPreviewerBottomContainer) { v, insets -> ViewCompat.setOnApplyWindowInsetsListener(views.attachmentPreviewerBottomContainer) { v, insets ->
v.updatePadding(bottom = insets.systemWindowInsetBottom) v.updatePadding(bottom = insets.systemWindowInsetBottom)
insets insets

View file

@ -0,0 +1,117 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.auth
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.extensions.showPassword
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentReauthConfirmBinding
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
class PromptFragment : VectorBaseFragment<FragmentReauthConfirmBinding>() {
private val viewModel: ReAuthViewModel by activityViewModel()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) =
FragmentReauthConfirmBinding.inflate(layoutInflater, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
views.reAuthConfirmButton.debouncedClicks {
onButtonClicked()
}
views.passwordReveal.debouncedClicks {
viewModel.handle(ReAuthActions.StartSSOFallback)
}
views.passwordReveal.debouncedClicks {
viewModel.handle(ReAuthActions.TogglePassVisibility)
}
}
private fun onButtonClicked() = withState(viewModel) { state ->
when (state.flowType) {
LoginFlowTypes.SSO -> {
viewModel.handle(ReAuthActions.StartSSOFallback)
}
LoginFlowTypes.PASSWORD -> {
val password = views.passwordField.text.toString()
if (password.isBlank()) {
// Prompt to enter something
views.passwordFieldTil.error = getString(R.string.error_empty_field_your_password)
} else {
views.passwordFieldTil.error = null
viewModel.handle(ReAuthActions.ReAuthWithPass(password))
}
}
else -> {
// not supported
}
}
}
override fun invalidate() = withState(viewModel) {
when (it.flowType) {
LoginFlowTypes.SSO -> {
views.passwordContainer.isVisible = false
views.reAuthConfirmButton.text = getString(R.string.auth_login_sso)
}
LoginFlowTypes.PASSWORD -> {
views.passwordContainer.isVisible = true
views.reAuthConfirmButton.text = getString(R.string._continue)
}
else -> {
// This login flow is not supported, you should use web?
}
}
views.passwordField.showPassword(it.passwordVisible)
if (it.passwordVisible) {
views.passwordReveal.setImageResource(R.drawable.ic_eye_closed)
views.passwordReveal.contentDescription = getString(R.string.a11y_hide_password)
} else {
views.passwordReveal.setImageResource(R.drawable.ic_eye)
views.passwordReveal.contentDescription = getString(R.string.a11y_show_password)
}
if (it.lastErrorCode != null) {
when (it.flowType) {
LoginFlowTypes.SSO -> {
views.genericErrorText.isVisible = true
views.genericErrorText.text = getString(R.string.authentication_error)
}
LoginFlowTypes.PASSWORD -> {
views.passwordFieldTil.error = getString(R.string.authentication_error)
}
else -> {
// nop
}
}
} else {
views.passwordFieldTil.error = null
views.genericErrorText.isVisible = false
}
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.auth
import im.vector.app.core.platform.VectorViewModelAction
sealed class ReAuthActions : VectorViewModelAction {
object StartSSOFallback : ReAuthActions()
object FallBackPageLoaded : ReAuthActions()
object FallBackPageClosed : ReAuthActions()
object TogglePassVisibility : ReAuthActions()
data class ReAuthWithPass(val password: String) : ReAuthActions()
}

View file

@ -0,0 +1,228 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.auth
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import androidx.browser.customtabs.CustomTabsCallback
import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsServiceConnection
import androidx.browser.customtabs.CustomTabsSession
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.addFragment
import im.vector.app.core.platform.SimpleFragmentActivity
import im.vector.app.core.utils.openUrlInChromeCustomTab
import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
import timber.log.Timber
import javax.inject.Inject
class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory {
@Parcelize
data class Args(
val flowType: String?,
val title: String?,
val session: String?,
val lastErrorCode: String?,
val resultKeyStoreAlias: String
) : Parcelable
// For sso
private var customTabsServiceConnection: CustomTabsServiceConnection? = null
private var customTabsClient: CustomTabsClient? = null
private var customTabsSession: CustomTabsSession? = null
@Inject lateinit var authenticationService: AuthenticationService
@Inject lateinit var reAuthViewModelFactory: ReAuthViewModel.Factory
override fun create(initialState: ReAuthState) = reAuthViewModelFactory.create(initialState)
override fun injectWith(injector: ScreenComponent) {
super.injectWith(injector)
injector.inject(this)
}
private val sharedViewModel: ReAuthViewModel by viewModel()
// override fun getTitleRes() = R.string.re_authentication_activity_title
override fun initUiAndData() {
super.initUiAndData()
val title = intent.extras?.getString(EXTRA_REASON_TITLE) ?: getString(R.string.re_authentication_activity_title)
supportActionBar?.setTitle(title) ?: run { setTitle(title) }
// val authArgs = intent.getParcelableExtra<Args>(MvRx.KEY_ARG)
// For the sso flow we can for now only rely on the fallback flow, that handles all
// the UI, due to the sandbox nature of CCT (chrome custom tab) we cannot get much information
// on how the process did go :/
// so we assume that after the user close the tab we return success and let caller retry the UIA flow :/
if (isFirstCreation()) {
addFragment(
R.id.container,
PromptFragment::class.java
)
}
sharedViewModel.observeViewEvents {
when (it) {
is ReAuthEvents.OpenSsoURl -> {
openInCustomTab(it.url)
}
ReAuthEvents.Dismiss -> {
setResult(RESULT_CANCELED)
finish()
}
is ReAuthEvents.PasswordFinishSuccess -> {
setResult(RESULT_OK, Intent().apply {
putExtra(RESULT_FLOW_TYPE, LoginFlowTypes.PASSWORD)
putExtra(RESULT_VALUE, it.passwordSafeForIntent)
})
finish()
}
}
}
}
override fun onResume() {
super.onResume()
// It's the only way we have to know if sso falback flow was successful
withState(sharedViewModel) {
if (it.ssoFallbackPageWasShown) {
Timber.d("## UIA ssoFallbackPageWasShown tentative success")
setResult(RESULT_OK, Intent().apply {
putExtra(RESULT_FLOW_TYPE, LoginFlowTypes.SSO)
})
finish()
}
}
}
override fun onStart() {
super.onStart()
withState(sharedViewModel) { state ->
if (state.ssoFallbackPageWasShown) {
sharedViewModel.handle(ReAuthActions.FallBackPageClosed)
return@withState
}
}
val packageName = CustomTabsClient.getPackageName(this, null)
// packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device
if (packageName != null) {
customTabsServiceConnection = object : CustomTabsServiceConnection() {
override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
Timber.d("## CustomTab onCustomTabsServiceConnected($name)")
customTabsClient = client
.also { it.warmup(0L) }
customTabsSession = customTabsClient?.newSession(object : CustomTabsCallback() {
// override fun onPostMessage(message: String, extras: Bundle?) {
// Timber.v("## CustomTab onPostMessage($message)")
// }
//
// override fun onMessageChannelReady(extras: Bundle?) {
// Timber.v("## CustomTab onMessageChannelReady()")
// }
override fun onNavigationEvent(navigationEvent: Int, extras: Bundle?) {
Timber.v("## CustomTab onNavigationEvent($navigationEvent), $extras")
super.onNavigationEvent(navigationEvent, extras)
if (navigationEvent == NAVIGATION_FINISHED) {
// sharedViewModel.handle(ReAuthActions.FallBackPageLoaded)
}
}
override fun onRelationshipValidationResult(relation: Int, requestedOrigin: Uri, result: Boolean, extras: Bundle?) {
Timber.v("## CustomTab onRelationshipValidationResult($relation), $requestedOrigin")
super.onRelationshipValidationResult(relation, requestedOrigin, result, extras)
}
})
}
override fun onServiceDisconnected(name: ComponentName?) {
Timber.d("## CustomTab onServiceDisconnected($name)")
}
}.also {
CustomTabsClient.bindCustomTabsService(
this,
// Despite the API, packageName cannot be null
packageName,
it
)
}
}
}
override fun onStop() {
super.onStop()
customTabsServiceConnection?.let { this.unbindService(it) }
customTabsServiceConnection = null
customTabsSession = null
}
private fun openInCustomTab(ssoUrl: String) {
openUrlInChromeCustomTab(this, customTabsSession, ssoUrl)
val channelOpened = customTabsSession?.requestPostMessageChannel(Uri.parse("https://element.io"))
Timber.d("## CustomTab channelOpened: $channelOpened")
}
companion object {
const val EXTRA_AUTH_TYPE = "EXTRA_AUTH_TYPE"
const val EXTRA_REASON_TITLE = "EXTRA_REASON_TITLE"
const val RESULT_FLOW_TYPE = "RESULT_FLOW_TYPE"
const val RESULT_VALUE = "RESULT_VALUE"
const val DEFAULT_RESULT_KEYSTORE_ALIAS = "ReAuthActivity"
fun newIntent(context: Context,
fromError: RegistrationFlowResponse,
lastErrorCode: String?,
reasonTitle: String?,
resultKeyStoreAlias: String = DEFAULT_RESULT_KEYSTORE_ALIAS): Intent {
val authType = when (fromError.nextUncompletedStage()) {
LoginFlowTypes.PASSWORD -> {
LoginFlowTypes.PASSWORD
}
LoginFlowTypes.SSO -> {
LoginFlowTypes.SSO
}
else -> {
// TODO, support more auth type?
null
}
}
return Intent(context, ReAuthActivity::class.java).apply {
putExtra(MvRx.KEY_ARG, Args(authType, reasonTitle, fromError.session, lastErrorCode, resultKeyStoreAlias))
}
}
}
}

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2020 New Vector Ltd * Copyright (c) 2021 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,6 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.core.error package im.vector.app.features.auth
class SsoFlowNotSupportedYet : Throwable() import im.vector.app.core.platform.VectorViewEvents
sealed class ReAuthEvents : VectorViewEvents {
data class OpenSsoURl(val url: String) : ReAuthEvents()
object Dismiss : ReAuthEvents()
data class PasswordFinishSuccess(val passwordSafeForIntent: String) : ReAuthEvents()
}

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.auth
import com.airbnb.mvrx.MvRxState
data class ReAuthState(
val title: String? = null,
val session: String? = null,
val flowType: String? = null,
val ssoFallbackPageWasShown: Boolean = false,
val passwordVisible: Boolean = false,
val lastErrorCode: String? = null,
val resultKeyStoreAlias: String = ""
) : MvRxState {
constructor(args: ReAuthActivity.Args) : this(
args.title,
args.session,
args.flowType,
lastErrorCode = args.lastErrorCode,
resultKeyStoreAlias = args.resultKeyStoreAlias
)
constructor() : this(null, null)
}

View file

@ -0,0 +1,85 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.auth
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.platform.VectorViewModel
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
import java.io.ByteArrayOutputStream
class ReAuthViewModel @AssistedInject constructor(
@Assisted val initialState: ReAuthState,
private val session: Session
) : VectorViewModel<ReAuthState, ReAuthActions, ReAuthEvents>(initialState) {
@AssistedFactory
interface Factory {
fun create(initialState: ReAuthState): ReAuthViewModel
}
companion object : MvRxViewModelFactory<ReAuthViewModel, ReAuthState> {
override fun create(viewModelContext: ViewModelContext, state: ReAuthState): ReAuthViewModel? {
val factory = when (viewModelContext) {
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
is ActivityViewModelContext -> viewModelContext.activity as? Factory
}
return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
}
}
override fun handle(action: ReAuthActions) = withState { state ->
when (action) {
ReAuthActions.StartSSOFallback -> {
if (state.flowType == LoginFlowTypes.SSO) {
setState { copy(ssoFallbackPageWasShown = true) }
val ssoURL = session.getUiaSsoFallbackUrl(initialState.session ?: "")
_viewEvents.post(ReAuthEvents.OpenSsoURl(ssoURL))
}
}
ReAuthActions.FallBackPageLoaded -> {
setState { copy(ssoFallbackPageWasShown = true) }
}
ReAuthActions.FallBackPageClosed -> {
// Should we do something here?
}
ReAuthActions.TogglePassVisibility -> {
setState {
copy(
passwordVisible = !state.passwordVisible
)
}
}
is ReAuthActions.ReAuthWithPass -> {
val safeForIntentCypher = ByteArrayOutputStream().also {
it.use {
session.securelyStoreObject(action.password, initialState.resultKeyStoreAlias, it)
}
}.toByteArray().toBase64NoPadding()
_viewEvents.post(ReAuthEvents.PasswordFinishSuccess(safeForIntentCypher))
}
}
}
}

View file

@ -48,7 +48,7 @@ class CallAudioManager(
private var savedIsSpeakerPhoneOn = false private var savedIsSpeakerPhoneOn = false
private var savedIsMicrophoneMute = false private var savedIsMicrophoneMute = false
private var savedAudioMode = AudioManager.MODE_INVALID private var savedAudioMode = AudioManager.MODE_NORMAL
private var connectedBlueToothHeadset: BluetoothProfile? = null private var connectedBlueToothHeadset: BluetoothProfile? = null
private var wantsBluetoothConnection = false private var wantsBluetoothConnection = false

View file

@ -25,8 +25,11 @@ import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.View import android.view.View
import android.view.Window import android.view.Window
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.view.WindowManager import android.view.WindowManager
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
@ -102,11 +105,24 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
setContentView(R.layout.activity_call) setContentView(R.layout.activity_call)
} }
@Suppress("DEPRECATION")
private fun hideSystemUI() { private fun hideSystemUI() {
systemUiVisibility = false systemUiVisibility = false
// Enables regular immersive mode. // Enables regular immersive mode.
// For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE. // For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE.
// Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY // Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
window.setDecorFitsSystemWindows(false)
// New API instead of SYSTEM_UI_FLAG_HIDE_NAVIGATION
window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars())
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
// New API instead of FLAG_TRANSLUCENT_STATUS
window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
// New API instead of FLAG_TRANSLUCENT_NAVIGATION
window.navigationBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
} else {
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
// Set the content to appear under the system bars so that the // Set the content to appear under the system bars so that the
// content doesn't resize when the system bars hide and show. // content doesn't resize when the system bars hide and show.
@ -117,15 +133,22 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN) or View.SYSTEM_UI_FLAG_FULLSCREEN)
} }
}
// Shows the system bars by removing all the flags // Shows the system bars by removing all the flags
// except for the ones that make the content appear under the system bars. // except for the ones that make the content appear under the system bars.
@Suppress("DEPRECATION")
private fun showSystemUI() { private fun showSystemUI() {
systemUiVisibility = true systemUiVisibility = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
window.setDecorFitsSystemWindows(false)
} else {
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
} }
}
private fun toggleUiSystemVisibility() { private fun toggleUiSystemVisibility() {
if (systemUiVisibility) { if (systemUiVisibility) {

Some files were not shown because too many files have changed in this diff Show more