[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 ✨:
@ -23,6 +23,36 @@ Build 🧱:
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:
- Update Dagger to 2.31 version so we can use the embedded AssistedInject feature

View file

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

View file

@ -18,15 +18,19 @@
package im.vector.lib.attachmentviewer
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.view.WindowManager
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.GestureDetectorCompat
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
@ -94,14 +98,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// This is important for the dispatchTouchEvent, if not we must correct
// 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)
setDecorViewFullScreen()
views = ActivityAttachmentViewerBinding.inflate(layoutInflater)
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) {
attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition)?.let {
(it as? BaseViewHolder)?.onSelected(false)
@ -313,28 +333,48 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
?.handleCommand(commands)
}
@Suppress("DEPRECATION")
private fun hideSystemUI() {
systemUiVisibility = false
// Enables regular immersive mode.
// For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE.
// Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
// Set the content to appear under the system bars so that the
// content doesn't resize when the system bars hide and show.
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
// Hide the nav bar and status bar
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN)
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
// Set the content to appear under the system bars so that the
// content doesn't resize when the system bars hide and show.
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
// Hide the nav bar and status bar
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN)
}
}
// Shows the system bars by removing all the flags
// except for the ones that make the content appear under the system bars.
@Suppress("DEPRECATION")
private fun showSystemUI() {
systemUiVisibility = true
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
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
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
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
distributionPath=wrapper/dists
#distributionSha256Sum=a7ca23b3ccf265680f2bfd35f1f00b1424f4466292c7337c85d46c9641b3f053
distributionSha256Sum=3db89524a3981819ff28c3f979236c1274a726e146ced0c8a2020417f9bc0782
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-all.zip
zipStoreBase=GRADLE_USER_HOME
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'
android {
compileSdkVersion 29
compileSdkVersion 30
defaultConfig {
minSdkVersion 21
targetSdkVersion 29
targetSdkVersion 30
versionCode 1
versionName "1.0"

View file

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

View file

@ -16,8 +16,18 @@
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.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.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.auth.registration.RegistrationResult
import org.matrix.android.sdk.api.failure.Failure
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.TestConstants
import org.matrix.android.sdk.common.TestMatrixCallback
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 kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@ -44,7 +50,18 @@ class DeactivateAccountTest : InstrumentedTest {
// Deactivate the account
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)

View file

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

View file

@ -19,6 +19,18 @@ package org.matrix.android.sdk.common
import android.os.SystemClock
import android.util.Log
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.crypto.verification.IncomingSasVerificationTransaction
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.keysbackup.model.MegolmBackupAuthData
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.concurrent.CountDownLatch
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
@ -304,10 +309,18 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
fun initializeCrossSigning(session: Session) {
mTestHelper.doSync<Unit> {
session.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = session.myUserId,
password = TestConstants.PASSWORD
), it)
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = session.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, it)
}
}

View file

@ -17,7 +17,18 @@
package org.matrix.android.sdk.internal.crypto
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.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.MXCryptoError
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.internal.crypto.model.OlmSessionWrapper
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.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 timber.log.Timber
import java.util.concurrent.CountDownLatch
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
/**
* Ref:
@ -202,10 +207,18 @@ class UnwedgingTest : InstrumentedTest {
// It's a trick to force key request on fail to decrypt
mTestHelper.doSync<Unit> {
bobSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = bobSession.myUserId,
password = TestConstants.PASSWORD
), it)
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = bobSession.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, it)
}
// Wait until we received back the key

View file

@ -17,14 +17,6 @@
package org.matrix.android.sdk.internal.crypto.crosssigning
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.assertFalse
import org.junit.Assert.assertNotNull
@ -35,6 +27,19 @@ 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.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)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@ -49,10 +54,17 @@ class XSigningTest : InstrumentedTest {
mTestHelper.doSync<Unit> {
aliceSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = aliceSession.myUserId,
password = TestConstants.PASSWORD
), it)
.initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = aliceSession.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, it)
}
val myCrossSigningKeys = aliceSession.cryptoService().crossSigningService().getMyCrossSigningKeys()
@ -86,8 +98,18 @@ class XSigningTest : InstrumentedTest {
password = TestConstants.PASSWORD
)
mTestHelper.doSync<Unit> { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(aliceAuthParams, it) }
mTestHelper.doSync<Unit> { bobSession.cryptoService().crossSigningService().initializeCrossSigning(bobAuthParams, it) }
mTestHelper.doSync<Unit> {
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
mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) }
@ -122,8 +144,16 @@ class XSigningTest : InstrumentedTest {
password = TestConstants.PASSWORD
)
mTestHelper.doSync<Unit> { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(aliceAuthParams, it) }
mTestHelper.doSync<Unit> { bobSession.cryptoService().crossSigningService().initializeCrossSigning(bobAuthParams, it) }
mTestHelper.doSync<Unit> { 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
val bobUserId = bobSession.myUserId

View file

@ -18,7 +18,21 @@ package org.matrix.android.sdk.internal.crypto.gossiping
import android.util.Log
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.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.SasVerificationTransaction
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.room.model.RoomDirectoryVisibility
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.CryptoTestHelper
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.MXUsersDevicesMap
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 kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@ -200,10 +205,17 @@ class KeyShareTests : InstrumentedTest {
mTestHelper.doSync<Unit> {
aliceSession1.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = aliceSession1.myUserId,
password = TestConstants.PASSWORD
), it)
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = aliceSession1.myUserId,
password = TestConstants.PASSWORD
)
)
}
}, it)
}
// Also bootstrap keybackup on first session
@ -305,10 +317,18 @@ class KeyShareTests : InstrumentedTest {
mTestHelper.doSync<Unit> {
aliceSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = aliceSession.myUserId,
password = TestConstants.PASSWORD
), it)
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = aliceSession.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, it)
}
// 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))
mTestHelper.doSync<Unit> {
bobSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = bobSession.myUserId,
password = TestConstants.PASSWORD
), it)
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = bobSession.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, it)
}
// Let alice invite bob
@ -356,7 +384,7 @@ class KeyShareTests : InstrumentedTest {
val roomRoomBobPov = aliceSession.getRoom(roomId)
val beforeJoin = roomRoomBobPov!!.getTimeLineEvent(secondEventId)
var dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "") }
var dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "") }
assert(dRes == null)
@ -367,7 +395,7 @@ class KeyShareTests : InstrumentedTest {
Thread.sleep(3_000)
// With the bug the first session would have improperly reshare that key :/
dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin.root, "") }
dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin.root, "") }
Log.d("#TEST", "KS: sgould not decrypt that ${beforeJoin.root.getClearContent().toModel<MessageContent>()?.body}")
assert(dRes?.clearEvent == null)
}

View file

@ -17,20 +17,25 @@
package org.matrix.android.sdk.internal.crypto.verification.qrcode
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.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.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 kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@ -157,18 +162,34 @@ class VerificationTest : InstrumentedTest {
mTestHelper.doSync<Unit> { callback ->
aliceSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = aliceSession.myUserId,
password = TestConstants.PASSWORD
), callback)
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = aliceSession.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, callback)
}
mTestHelper.doSync<Unit> { callback ->
bobSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = bobSession.myUserId,
password = TestConstants.PASSWORD
), callback)
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = bobSession.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, callback)
}
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.room.model.message.MessageTextContent
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)
internal class UrlsExtractorTest : InstrumentedTest {
@ -36,6 +38,7 @@ internal class UrlsExtractorTest : InstrumentedTest {
fun wrongEventTypeTest() {
createEvent(body = "https://matrix.org")
.copy(type = EventType.STATE_ROOM_GUEST_ACCESS)
.toFakeTimelineEvent()
.let { urlsExtractor.extract(it) }
.size shouldBeEqualTo 0
}
@ -43,6 +46,7 @@ internal class UrlsExtractorTest : InstrumentedTest {
@Test
fun oneUrlTest() {
createEvent(body = "https://matrix.org")
.toFakeTimelineEvent()
.let { urlsExtractor.extract(it) }
.let { result ->
result.size shouldBeEqualTo 1
@ -53,6 +57,7 @@ internal class UrlsExtractorTest : InstrumentedTest {
@Test
fun withoutProtocolTest() {
createEvent(body = "www.matrix.org")
.toFakeTimelineEvent()
.let { urlsExtractor.extract(it) }
.size shouldBeEqualTo 0
}
@ -60,6 +65,7 @@ internal class UrlsExtractorTest : InstrumentedTest {
@Test
fun oneUrlWithParamTest() {
createEvent(body = "https://matrix.org?foo=bar")
.toFakeTimelineEvent()
.let { urlsExtractor.extract(it) }
.let { result ->
result.size shouldBeEqualTo 1
@ -70,6 +76,7 @@ internal class UrlsExtractorTest : InstrumentedTest {
@Test
fun oneUrlWithParamsTest() {
createEvent(body = "https://matrix.org?foo=bar&bar=foo")
.toFakeTimelineEvent()
.let { urlsExtractor.extract(it) }
.let { result ->
result.size shouldBeEqualTo 1
@ -80,16 +87,18 @@ internal class UrlsExtractorTest : InstrumentedTest {
@Test
fun oneUrlInlinedTest() {
createEvent(body = "Hello https://matrix.org, how are you?")
.toFakeTimelineEvent()
.let { urlsExtractor.extract(it) }
.let { result ->
result.size shouldBeEqualTo 1
result[0] shouldBeEqualTo "https://matrix.org"
result[0] shouldBeEqualTo "https://matrix.org"
}
}
@Test
fun twoUrlsTest() {
createEvent(body = "https://matrix.org https://example.org")
.toFakeTimelineEvent()
.let { urlsExtractor.extract(it) }
.let { result ->
result.size shouldBeEqualTo 2
@ -99,10 +108,26 @@ internal class UrlsExtractorTest : InstrumentedTest {
}
private fun createEvent(body: String): Event = Event(
eventId = "!fake",
type = EventType.MESSAGE,
content = MessageTextContent(
msgType = MessageType.MSGTYPE_TEXT,
body = body
).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)
// Alice clear the cache
commonTestHelper.doSync<Unit> {
aliceSession.clearCache(it)
commonTestHelper.runBlockingTest {
aliceSession.clearCache()
}
// 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
* 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.JsonClass
@ -27,7 +27,7 @@ data class UserPasswordAuth(
// device device session id
@Json(name = "session")
val session: String? = null,
override val session: String? = null,
// registration information
@Json(name = "type")
@ -38,4 +38,16 @@ data class UserPasswordAuth(
@Json(name = "password")
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.
* 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 {
companion object {
// Not really defined by the spec, but we may define some ids here
const val ID_GOOGLE = "google"
const val ID_GITHUB = "github"
const val ID_APPLE = "apple"
const val ID_FACEBOOK = "facebook"
const val ID_TWITTER = "twitter"
const val BRAND_GOOGLE = "org.matrix.google"
const val BRAND_GITHUB = "org.matrix.github"
const val BRAND_APPLE = "org.matrix.apple"
const val BRAND_FACEBOOK = "org.matrix.facebook"
const val BRAND_TWITTER = "org.matrix.twitter"
const val BRAND_GITLAB = "org.matrix.gitlab"
}
}

View file

@ -14,14 +14,11 @@
* 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.JsonClass
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.internal.auth.data.InteractiveAuthenticationFlow
@ -109,3 +106,8 @@ fun RegistrationFlowResponse.toFlowResult(): FlowResult {
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
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
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 java.io.IOException
import javax.net.ssl.HttpsURLConnection
@ -43,6 +43,12 @@ fun Throwable.isInvalidPassword(): Boolean {
&& 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
*/
@ -53,6 +59,16 @@ fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? {
.adapter(RegistrationFlowResponse::class.java)
.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 {
null
}

View file

@ -16,8 +16,8 @@
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.internal.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.internal.network.ssl.Fingerprint
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.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.
@ -42,7 +44,17 @@ data class MatrixError(
@Json(name = "soft_logout") val isSoftLogout: Boolean = false,
// For 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 {

View file

@ -245,6 +245,8 @@ interface Session :
val sharedSecretStorageService: SharedSecretStorageService
fun getUiaSsoFallbackUrl(authenticationSessionId: String): String
/**
* 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
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
/**
* 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
* 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
import org.matrix.android.sdk.api.MatrixCallback
/**
* 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.
*/
fun clearCache(callback: MatrixCallback<Unit>)
suspend fun clearCache()
}

View file

@ -20,6 +20,7 @@ import android.content.Context
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
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.session.crypto.crosssigning.CrossSigningService
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 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>)

View file

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

View file

@ -17,15 +17,16 @@
package org.matrix.android.sdk.api.session.media
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
interface MediaService {
/**
* Extract URLs from an Event.
* @return the list of URLs contains in the body of the Event. It does not mean that URLs in this list have UrlPreview data
* Extract URLs from a TimelineEvent.
* @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

View file

@ -20,6 +20,7 @@ package org.matrix.android.sdk.api.session.profile
import android.net.Uri
import androidx.lifecycle.LiveData
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.util.Cancelable
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
*/
fun finalizeAddingThreePid(threePid: ThreePid,
uiaSession: String?,
accountPassword: String?,
userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor,
matrixCallback: MatrixCallback<Unit>): Cancelable
/**

View file

@ -89,6 +89,17 @@ data class TimelineEvent(
*/
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
*/

View file

@ -16,9 +16,7 @@
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.util.Cancelable
/**
* 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.
* The same deviceId will be used
*/
fun signInAgain(password: String,
callback: MatrixCallback<Unit>): Cancelable
suspend fun signInAgain(password: String)
/**
* Update the session with credentials received after SSO
*/
fun updateCredentials(credentials: Credentials,
callback: MatrixCallback<Unit>): Cancelable
suspend fun updateCredentials(credentials: Credentials)
/**
* 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
*/
fun signOut(signOutFromHomeserver: Boolean,
callback: MatrixCallback<Unit>): Cancelable
suspend fun signOut(signOutFromHomeserver: Boolean)
}

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 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
*/
@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.RegistrationResult
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.RegistrationFlowError
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 org.matrix.android.sdk.api.MatrixCallback
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.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.Failure
@ -207,9 +208,9 @@ internal class DefaultCryptoService @Inject constructor(
.executeBy(taskExecutor)
}
override fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) {
override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>) {
deleteDeviceTask
.configureWith(DeleteDeviceTask.Params(deviceId)) {
.configureWith(DeleteDeviceTask.Params(deviceId, userInteractiveAuthInterceptor, null)) {
this.executionThread = TaskThread.CRYPTO
this.callback = callback
}

View file

@ -19,30 +19,30 @@ package org.matrix.android.sdk.internal.crypto.crosssigning
import androidx.lifecycle.LiveData
import androidx.work.BackoffPolicy
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.auth.UserInteractiveAuthInterceptor
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.MXCrossSigningInfo
import org.matrix.android.sdk.api.util.Optional
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.UserPasswordAuth
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.tasks.InitializeCrossSigningTask
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.WorkManagerProvider
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.TaskThread
import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
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.olm.OlmPkSigning
import org.matrix.olm.OlmUtility
@ -61,7 +61,10 @@ internal class DefaultCrossSigningService @Inject constructor(
private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
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
@ -147,11 +150,11 @@ internal class DefaultCrossSigningService @Inject constructor(
* - Sign the keys and upload them
* - 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")
val params = InitializeCrossSigningTask.Params(
authParams = authParams
interactiveAuthInterceptor = uiaInterceptor
)
initializeCrossSigningTask.configureWith(params) {
this.callbackThread = TaskThread.CRYPTO
@ -689,7 +692,7 @@ internal class DefaultCrossSigningService @Inject constructor(
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()
myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId))
@ -747,8 +750,11 @@ internal class DefaultCrossSigningService @Inject constructor(
}
override fun onUsersDeviceUpdate(userIds: List<String>) {
Timber.d("## CrossSigning - onUsersDeviceUpdate for $userIds")
val workerParams = UpdateTrustWorker.Params(sessionId = sessionId, updatedUserIds = userIds)
Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users: $userIds")
val workerParams = UpdateTrustWorker.Params(
sessionId = sessionId,
filename = updateTrustWorkerDataRepository.createParam(userIds)
)
val workerData = WorkerParamsFactory.toData(workerParams)
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<UpdateTrustWorker>()

View file

@ -55,7 +55,11 @@ internal class UpdateTrustWorker(context: Context,
internal data class Params(
override val sessionId: String,
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
@Inject lateinit var crossSigningService: DefaultCrossSigningService
@ -64,6 +68,7 @@ internal class UpdateTrustWorker(context: Context,
@CryptoDatabase @Inject lateinit var realmConfiguration: RealmConfiguration
@UserId @Inject lateinit var myUserId: String
@Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper
@Inject lateinit var updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
@SessionDatabase @Inject lateinit var sessionRealmConfiguration: RealmConfiguration
// @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater
@ -74,7 +79,17 @@ internal class UpdateTrustWorker(context: Context,
}
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,
// or a new device?) So we check all again :/
@ -213,9 +228,15 @@ internal class UpdateTrustWorker(context: Context,
}
}
cleanup(params)
return Result.success()
}
private fun cleanup(params: Params) {
params.filename
?.let { updateTrustWorkerDataRepository.delete(it) }
}
private fun updateCrossSigningKeysTrust(realm: Realm, userId: String, verified: Boolean) {
val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java)
.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)
internal data class DeleteDeviceParams(
@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,
@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
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
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.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.executeRequest
import org.matrix.android.sdk.internal.task.Task
import timber.log.Timber
import javax.inject.Inject
internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> {
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) {
try {
executeRequest<Unit>(globalErrorReceiver) {
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams())
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams(params.userAuthParam?.asMap()))
}
} catch (throwable: Throwable) {
throw throwable.toRegistrationFlowResponse()
?.let { Failure.RegistrationFlowError(it) }
?: throwable
if (params.userInteractiveAuthInterceptor == null
|| !handleUIA(throwable, params.userInteractiveAuthInterceptor) { auth ->
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.internal.crypto.api.CryptoApi
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.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
@ -44,12 +44,12 @@ internal class DefaultDeleteDeviceWithUserPasswordTask @Inject constructor(
return executeRequest(globalErrorReceiver) {
apiCall = cryptoApi.deleteDevice(params.deviceId,
DeleteDeviceParams(
userPasswordAuth = UserPasswordAuth(
auth = UserPasswordAuth(
type = LoginFlowTypes.PASSWORD,
session = params.authSession,
user = userId,
password = params.password
)
).asMap()
)
)
}

View file

@ -17,6 +17,8 @@
package org.matrix.android.sdk.internal.crypto.tasks
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.MyDeviceInfoHolder
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.KeyUsage
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.task.Task
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
@ -34,7 +35,7 @@ import javax.inject.Inject
internal interface InitializeCrossSigningTask : Task<InitializeCrossSigningTask.Params, InitializeCrossSigningTask.Result> {
data class Params(
val authParams: UserPasswordAuth?
val interactiveAuthInterceptor: UserInteractiveAuthInterceptor?
)
data class Result(
@ -117,10 +118,21 @@ internal class DefaultInitializeCrossSigningTask @Inject constructor(
.key(sskPublicKey)
.signature(userId, masterPublicKey, signedSSK)
.build(),
userPasswordAuth = params.authParams
userAuthParam = null
// userAuthParam = params.authParams
)
uploadSigningKeysTask.execute(uploadSigningKeysParams)
try {
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
val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder()

View file

@ -16,14 +16,12 @@
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.toRegistrationFlowResponse
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.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.UserPasswordAuth
import org.matrix.android.sdk.internal.crypto.model.toRest
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
@ -39,15 +37,9 @@ internal interface UploadSigningKeysTask : Task<UploadSigningKeysTask.Params, Un
// the SSK
val selfSignedKey: CryptoCrossSigningKey,
/**
* - If null:
* - 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
* Authorisation info (User Interactive flow)
*/
val userPasswordAuth: UserPasswordAuth?
val userAuthParam: UIABaseAuth?
)
}
@ -59,31 +51,13 @@ internal class DefaultUploadSigningKeysTask @Inject constructor(
) : UploadSigningKeysTask {
override suspend fun execute(params: UploadSigningKeysTask.Params) {
val paramsHaveSessionId = params.userPasswordAuth?.session != null
val uploadQuery = UploadSigningKeysBody(
masterKey = params.masterKey.toRest(),
userSigningKey = params.userKey.toRest(),
selfSigningKey = params.selfSignedKey.toRest(),
// If sessionId is provided, use the userPasswordAuth
auth = params.userPasswordAuth.takeIf { paramsHaveSessionId }
auth = params.userAuthParam?.asMap()
)
try {
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
}
}
doRequest(uploadQuery)
}
private suspend fun doRequest(uploadQuery: UploadSigningKeysBody) {

View file

@ -20,7 +20,6 @@ import androidx.annotation.MainThread
import dagger.Lazy
import io.realm.RealmConfiguration
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.failure.GlobalError
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.user.UserService
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.crypto.DefaultCryptoService
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()
stopAnyBackgroundSync()
uiHandler.post {
lifecycleObservers.forEach { it.onClearCache() }
}
cacheService.get().clearCache(callback)
cacheService.get().clearCache()
workManagerProvider.cancelAllWorks()
}
@ -274,6 +275,18 @@ internal class DefaultSession @Inject constructor(
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() {
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.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.

View file

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

View file

@ -16,6 +16,9 @@
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.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
@ -27,8 +30,9 @@ import javax.inject.Inject
internal interface DeactivateAccountTask : Task<DeactivateAccountTask.Params, Unit> {
data class Params(
val password: String,
val eraseAllData: Boolean
val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor,
val eraseAllData: Boolean,
val userAuthParam: UIABaseAuth? = null
)
}
@ -41,12 +45,21 @@ internal class DefaultDeactivateAccountTask @Inject constructor(
) : DeactivateAccountTask {
override suspend fun execute(params: DeactivateAccountTask.Params) {
val deactivateAccountParams = DeactivateAccountParams.create(userId, params.password, params.eraseAllData)
val deactivateAccountParams = DeactivateAccountParams.create(params.userAuthParam, params.eraseAllData)
executeRequest<Unit>(globalErrorReceiver) {
apiCall = accountAPI.deactivate(deactivateAccountParams)
try {
executeRequest<Unit>(globalErrorReceiver) {
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
runCatching { identityDisconnectTask.execute(Unit) }
.onFailure { Timber.w(it, "Unable to disconnect identity server") }

View file

@ -16,6 +16,7 @@
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 javax.inject.Inject
@ -26,7 +27,7 @@ internal class DefaultAccountService @Inject constructor(private val changePassw
changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword))
}
override suspend fun deactivateAccount(password: String, eraseAllData: Boolean) {
deactivateAccountTask.execute(DeactivateAccountTask.Params(password, eraseAllData))
override suspend fun deactivateAccount(userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, eraseAllData: Boolean) {
deactivateAccountTask.execute(DeactivateAccountTask.Params(userInteractiveAuthInterceptor, eraseAllData))
}
}

View file

@ -16,23 +16,18 @@
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.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
import javax.inject.Inject
internal class DefaultCacheService @Inject constructor(@SessionDatabase
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()
clearCacheTask
.configureWith {
this.callback = callback
}
.executeBy(taskExecutor)
clearCacheTask.execute(Unit)
}
}

View file

@ -28,9 +28,8 @@ import java.io.File
import java.util.UUID
import javax.inject.Inject
internal class ImageCompressor @Inject constructor() {
internal class ImageCompressor @Inject constructor(private val context: Context) {
suspend fun compress(
context: Context,
imageFile: File,
desiredWidth: Int,
desiredHeight: Int,
@ -46,7 +45,7 @@ internal class ImageCompressor @Inject constructor() {
}
} ?: return@withContext imageFile
val destinationFile = createDestinationFile(context)
val destinationFile = createDestinationFile()
runCatching {
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)
}
}

View file

@ -47,22 +47,24 @@ internal object ThumbnailExtractor {
val mediaMetadataRetriever = MediaMetadataRetriever()
try {
mediaMetadataRetriever.setDataSource(context, attachment.queryUri)
val thumbnail = mediaMetadataRetriever.frameAtTime
val outputStream = ByteArrayOutputStream()
thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
val thumbnailWidth = thumbnail.width
val thumbnailHeight = thumbnail.height
val thumbnailSize = outputStream.size()
thumbnailData = ThumbnailData(
width = thumbnailWidth,
height = thumbnailHeight,
size = thumbnailSize.toLong(),
bytes = outputStream.toByteArray(),
mimeType = MimeTypes.Jpeg
)
thumbnail.recycle()
outputStream.reset()
mediaMetadataRetriever.frameAtTime?.let { thumbnail ->
val outputStream = ByteArrayOutputStream()
thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
val thumbnailWidth = thumbnail.width
val thumbnailHeight = thumbnail.height
val thumbnailSize = outputStream.size()
thumbnailData = ThumbnailData(
width = thumbnailWidth,
height = thumbnailHeight,
size = thumbnailSize.toLong(),
bytes = outputStream.toByteArray(),
mimeType = MimeTypes.Jpeg
)
thumbnail.recycle()
outputStream.reset()
} ?: run {
Timber.e("Cannot extract video thumbnail at %s", attachment.queryUri.toString())
}
} catch (e: Exception) {
Timber.e(e, "Cannot extract video thumbnail")
} finally {

View file

@ -156,7 +156,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
// Do not compress gif
&& attachment.mimeType != MimeTypes.Gif
&& 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 ->
// Get new Bitmap size
compressedFile.inputStream().use {

View file

@ -52,65 +52,60 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor(
val pepper = identityData.hashLookupPepper
val hashDetailResponse = if (pepper == null) {
// We need to fetch the hash details first
fetchAndStoreHashDetails(identityAPI)
fetchHashDetails(identityAPI)
.also { identityStore.setHashDetails(it) }
} else {
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
// Also, what we have in cache could be outdated, the identity server maybe now supports sha256
throw IdentityServiceError.BulkLookupSha256NotSupported
}
val hashedAddresses = withOlmUtility { olmUtility ->
params.threePids.map { threePid ->
base64ToBase64Url(
olmUtility.sha256(threePid.value.toLowerCase(Locale.ROOT)
+ " " + threePid.toMedium() + " " + hashDetailResponse.pepper)
)
}
}
val identityLookUpV2Response = lookUpInternal(identityAPI, hashedAddresses, hashDetailResponse, true)
val lookUpData = lookUpInternal(identityAPI, params.threePids, hashDetailResponse, true)
// 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,
hashedAddresses: List<String>,
threePids: List<ThreePid>,
hashDetailResponse: IdentityHashDetailResponse,
canRetry: Boolean): IdentityLookUpResponse {
canRetry: Boolean): LookUpData {
val hashedAddresses = getHashedAddresses(threePids, hashDetailResponse.pepper)
return try {
executeRequest(null) {
apiCall = identityAPI.lookup(IdentityLookUpParams(
hashedAddresses,
IdentityHashDetailResponse.ALGORITHM_SHA256,
hashDetailResponse.pepper
))
}
LookUpData(hashedAddresses,
executeRequest(null) {
apiCall = identityAPI.lookup(IdentityLookUpParams(
hashedAddresses,
IdentityHashDetailResponse.ALGORITHM_SHA256,
hashDetailResponse.pepper
))
})
} catch (failure: Throwable) {
// Catch invalid hash pepper and retry
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!
if (!failure.error.newLookupPepper.isNullOrEmpty()) {
val newHashDetailResponse = if (!failure.error.newLookupPepper.isNullOrEmpty()) {
// Store it and use it right now
hashDetailResponse.copy(pepper = failure.error.newLookupPepper)
.also { identityStore.setHashDetails(it) }
.let { lookUpInternal(identityAPI, hashedAddresses, it, false /* Avoid infinite loop */) }
} else {
// Retrieve the new hash details
val newHashDetailResponse = fetchAndStoreHashDetails(identityAPI)
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
// Also, what we have in cache is maybe outdated, the identity server maybe now support sha256
throw IdentityServiceError.BulkLookupSha256NotSupported
}
lookUpInternal(identityAPI, hashedAddresses, newHashDetailResponse, false /* Avoid infinite loop */)
fetchHashDetails(identityAPI)
}
.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
throw IdentityServiceError.BulkLookupSha256NotSupported
}
lookUpInternal(identityAPI, threePids, newHashDetailResponse, false /* Avoid infinite loop */)
} else {
// Other error
throw failure
@ -118,16 +113,29 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor(
}
}
private suspend fun fetchAndStoreHashDetails(identityAPI: IdentityAPI): IdentityHashDetailResponse {
return executeRequest<IdentityHashDetailResponse>(null) {
apiCall = identityAPI.hashDetails()
private fun getHashedAddresses(threePids: List<ThreePid>, pepper: String): List<String> {
return withOlmUtility { olmUtility ->
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> {
return identityLookUpResponse.mappings.keys.map { hashedAddress ->
FoundThreePid(threePids[hashedAddresses.indexOf(hashedAddress)], identityLookUpResponse.mappings[hashedAddress] ?: error(""))
private suspend fun fetchHashDetails(identityAPI: IdentityAPI): IdentityHashDetailResponse {
return executeRequest(null) {
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 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.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.internal.util.getOrPut
import javax.inject.Inject
@ -34,11 +35,12 @@ internal class DefaultMediaService @Inject constructor(
// Cache of extracted URLs
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) }
}
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 {
return getRawPreviewUrlTask.execute(GetRawPreviewUrlTask.Params(url, timestamp))

View file

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

View file

@ -22,6 +22,7 @@ import androidx.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy
import io.realm.kotlin.where
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.profile.ProfileService
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,
uiaSession: String?,
accountPassword: String?,
userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor,
matrixCallback: MatrixCallback<Unit>): Cancelable {
return finalizeAddingThreePidTask
.configureWith(FinalizeAddingThreePidTask.Params(
threePid = threePid,
session = uiaSession,
accountPassword = accountPassword,
userInteractiveAuthInterceptor = userInteractiveAuthInterceptor,
userWantsToCancel = false
)) {
callback = alsoRefresh(matrixCallback)
@ -189,8 +188,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
return finalizeAddingThreePidTask
.configureWith(FinalizeAddingThreePidTask.Params(
threePid = threePid,
session = null,
accountPassword = null,
userInteractiveAuthInterceptor = null,
userWantsToCancel = true
)) {
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.JsonClass
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
@JsonClass(generateAdapter = true)
internal data class FinalizeAddThreePidBody(
@ -37,5 +36,5 @@ internal data class FinalizeAddThreePidBody(
* Additional authentication information for the user-interactive authentication API.
*/
@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
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.toRegistrationFlowResponse
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.PendingThreePidEntityFields
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.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import timber.log.Timber
import javax.inject.Inject
internal abstract class FinalizeAddingThreePidTask : Task<FinalizeAddingThreePidTask.Params, Unit> {
data class Params(
val threePid: ThreePid,
val session: String?,
val accountPassword: String?,
val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?,
val userAuthParam: UIABaseAuth? = null,
val userWantsToCancel: Boolean
)
}
@ -62,20 +65,21 @@ internal class DefaultFinalizeAddingThreePidTask @Inject constructor(
val body = FinalizeAddThreePidBody(
clientSecret = pendingThreePids.clientSecret,
sid = pendingThreePids.sid,
auth = if (params.session != null && params.accountPassword != null) {
UserPasswordAuth(
session = params.session,
user = userId,
password = params.accountPassword
)
} else null
auth = params.userAuthParam?.asMap()
)
apiCall = profileAPI.finalizeAddThreePid(body)
}
} catch (throwable: Throwable) {
throw throwable.toRegistrationFlowResponse()
?.let { Failure.RegistrationFlowError(it) }
?: throwable
if (params.userInteractiveAuthInterceptor == null
|| !handleUIA(throwable, params.userInteractiveAuthInterceptor) { auth ->
execute(params.copy(userAuthParam = auth))
}
) {
Timber.d("## UIA: propagate failure")
throw throwable.toRegistrationFlowResponse()
?.let { Failure.RegistrationFlowError(it) }
?: throwable
}
}
}

View file

@ -143,9 +143,11 @@ internal class CreateRoomBodyBuilder @Inject constructor(
}
private suspend fun canEnableEncryption(params: CreateRoomParams): Boolean {
return (params.enableEncryptionIfInvitedUsersSupportIt
&& crossSigningService.isCrossSigningVerified()
&& params.invite3pids.isEmpty())
return params.enableEncryptionIfInvitedUsersSupportIt
// Parity with web, enable if users have encryption ready devices
// for now remove checks on cross signing and 3pid invites
// && crossSigningService.isCrossSigningVerified()
&& params.invite3pids.isEmpty()
&& params.invitedUserIds.isNotEmpty()
&& params.invitedUserIds.let { userIds ->
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>>) {
val params = FetchEditHistoryTask.Params(roomId, cryptoSessionInfoProvider.isRoomEncrypted(roomId), eventId)
val params = FetchEditHistoryTask.Params(roomId, eventId)
fetchEditHistoryTask
.configureWith(params) {
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.EventType
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.executeRequest
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
internal interface FetchEditHistoryTask : Task<FetchEditHistoryTask.Params, List<Event>> {
data class Params(
val roomId: String,
val isRoomEncrypted: Boolean,
val eventId: String
)
}
internal class DefaultFetchEditHistoryTask @Inject constructor(
private val roomAPI: RoomAPI,
private val globalErrorReceiver: GlobalErrorReceiver
private val globalErrorReceiver: GlobalErrorReceiver,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider
) : FetchEditHistoryTask {
override suspend fun execute(params: FetchEditHistoryTask.Params): List<Event> {
val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
val response = executeRequest<RelationsResponse>(globalErrorReceiver) {
apiCall = roomAPI.getRelations(params.roomId,
params.eventId,
RelationType.REPLACE,
if (params.isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE)
apiCall = roomAPI.getRelations(
roomId = params.roomId,
eventId = params.eventId,
relationType = RelationType.REPLACE,
eventType = if (isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE
)
}
val events = response.chunks.toMutableList()

View file

@ -140,14 +140,13 @@ internal class RoomSummaryUpdater @Inject constructor(
.queryActiveRoomMembersEvent()
.notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId)
.findAll()
.asSequence()
.map { it.userId }
roomSummaryEntity.otherMemberIds.clear()
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
if (roomSummaryEntity.isEncrypted) {
// 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
@JsonClass(generateAdapter = true)
data class EventContextResponse(
internal data class EventContextResponse(
/**
* Details of the requested 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 = "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 = "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 {
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)
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,
/**
* 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 = "chunk") override val events: List<Event> = emptyList(),
@Json(name = "state") override val stateEvents: List<Event> = emptyList()
) : TokenChunkEvent
/**
* A list of room events. The order depends on the dir parameter. For dir=b events will be in
* 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 end: String?
val events: List<Event>
val stateEvents: List<Event>
val stateEvents: List<Event>?
fun hasMore() = start != end
}

View file

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

View file

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

View file

@ -16,45 +16,25 @@
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.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.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
internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask,
private val signInAgainTask: SignInAgainTask,
private val sessionParamsStore: SessionParamsStore,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor) : SignOutService {
private val sessionParamsStore: SessionParamsStore
) : SignOutService {
override fun signInAgain(password: String,
callback: MatrixCallback<Unit>): Cancelable {
return signInAgainTask
.configureWith(SignInAgainTask.Params(password)) {
this.callback = callback
}
.executeBy(taskExecutor)
override suspend fun signInAgain(password: String) {
signInAgainTask.execute(SignInAgainTask.Params(password))
}
override fun updateCredentials(credentials: Credentials,
callback: MatrixCallback<Unit>): Cancelable {
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
sessionParamsStore.updateCredentials(credentials)
}
override suspend fun updateCredentials(credentials: Credentials) {
sessionParamsStore.updateCredentials(credentials)
}
override fun signOut(signOutFromHomeserver: Boolean,
callback: MatrixCallback<Unit>): Cancelable {
return signOutTask
.configureWith(SignOutTask.Params(signOutFromHomeserver)) {
this.callback = callback
}
.executeBy(taskExecutor)
override suspend fun signOut(signOutFromHomeserver: Boolean) {
return signOutTask.execute(SignOutTask.Params(signOutFromHomeserver))
}
}

View file

@ -53,7 +53,7 @@ internal class WidgetFactory @Inject constructor(private val userDataSource: Use
}
}
val isAddedByMe = widgetEvent.senderId == userId
val computedUrl = widgetContent.computeURL(widgetEvent.roomId)
val computedUrl = widgetContent.computeURL(widgetEvent.roomId, widgetId)
return Widget(
widgetContent = widgetContent,
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
val myUser = userDataSource.getUser(userId)
computedUrl = computedUrl
.replace("\$matrix_user_id", userId)
.replace("\$matrix_display_name", myUser?.displayName ?: userId)
.replace("\$matrix_avatar_url", myUser?.avatarUrl ?: "")
.replace("\$matrix_widget_id", widgetId)
if (roomId != null) {
computedUrl = computedUrl.replace("\$matrix_room_id", roomId)

View file

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

View file

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

View file

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

View file

@ -13,7 +13,7 @@ kapt {
// Note: 2 digits max for each value
ext.versionMajor = 1
ext.versionMinor = 0
ext.versionPatch = 15
ext.versionPatch = 16
static def getGitTimestamp() {
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
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
// Ref: https://issuetracker.google.com/issues/144111441
@ -111,7 +111,7 @@ android {
applicationId "im.vector.app"
// Set to API 21: see #405
minSdkVersion 21
targetSdkVersion 29
targetSdkVersion 30
multiDexEnabled true
// `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.runner.RunWith
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.crypto.verification.SasVerificationTransaction
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.VerificationTransaction
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)
@LargeTest
@ -67,10 +72,18 @@ class VerifySessionInteractiveTest : VerificationTestBase() {
existingSession = createAccountAndSync(matrix, userName, password, true)
doSync<Unit> {
existingSession!!.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = existingSession!!.myUserId,
password = "password"
), it)
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = existingSession!!.myUserId,
password = "password",
session = flowResponse.session
)
)
}
}, it)
}
}

View file

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

View file

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

View file

@ -63,7 +63,6 @@
<activity
android:name=".features.MainActivity"
android:taskAffinity=""
android:theme="@style/AppTheme.Launcher" />
<!-- 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.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 -->
<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.SharedSecuredStoragePassphraseFragment
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.BootstrapConfirmPassphraseFragment
import im.vector.app.features.crypto.recover.BootstrapEnterPassphraseFragment
@ -522,8 +522,8 @@ interface FragmentModule {
@Binds
@IntoMap
@FragmentKey(BootstrapAccountPasswordFragment::class)
fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment
@FragmentKey(BootstrapReAuthFragment::class)
fun bindBootstrapReAuthFragment(fragment: BootstrapReAuthFragment): Fragment
@Binds
@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.preference.UserAvatarPreference
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.VectorCallActivity
import im.vector.app.features.call.conference.VectorJitsiActivity
@ -145,6 +146,7 @@ interface ScreenComponent {
fun inject(activity: VectorJitsiActivity)
fun inject(activity: SearchActivity)
fun inject(activity: UserCodeActivity)
fun inject(activity: ReAuthActivity)
/* ==========================================================================================
* BottomSheets

View file

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

View file

@ -19,10 +19,12 @@ package im.vector.app.core.platform
import android.app.Activity
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.WindowInsetsController
import android.view.WindowManager
import android.widget.TextView
import androidx.annotation.AttrRes
@ -33,6 +35,7 @@ import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentFactory
@ -410,13 +413,25 @@ abstract class VectorBaseActivity<VB: ViewBinding> : AppCompatActivity(), HasScr
/**
* Force to render the activity in fullscreen
*/
@Suppress("DEPRECATION")
private fun setFullScreen() {
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.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_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
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
}
}
/* ==========================================================================================

View file

@ -200,6 +200,7 @@ abstract class VectorBaseFragment<VB: ViewBinding> : BaseMvRxFragment(), HasScre
}
protected fun showLoadingDialog(message: CharSequence? = null, cancelable: Boolean = false) {
progress?.dismiss()
progress = ProgressDialog(requireContext()).apply {
setCancelable(cancelable)
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 androidx.appcompat.app.AlertDialog
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide
import im.vector.app.R
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 kotlinx.parcelize.Parcelize
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.failure.GlobalError
import timber.log.Timber
import javax.inject.Inject
@ -147,38 +146,39 @@ class MainActivity : VectorBaseActivity<FragmentLoadingBinding>(), UnlockedActiv
}
when {
args.isAccountDeactivated -> {
// Just do the local cleanup
Timber.w("Account deactivated, start app")
sessionHolder.clearActiveSession()
doLocalCleanup(clearPreferences = true)
startNextActivityAndFinish()
lifecycleScope.launch {
// Just do the local cleanup
Timber.w("Account deactivated, start app")
sessionHolder.clearActiveSession()
doLocalCleanup(clearPreferences = true)
startNextActivityAndFinish()
}
}
args.clearCredentials -> {
lifecycleScope.launch {
try {
session.signOut(!args.isUserLoggedOut)
Timber.w("SIGN_OUT: success, start app")
sessionHolder.clearActiveSession()
doLocalCleanup(clearPreferences = true)
startNextActivityAndFinish()
} catch (failure: Throwable) {
displayError(failure)
}
}
}
args.clearCache -> {
lifecycleScope.launch {
try {
session.clearCache()
doLocalCleanup(clearPreferences = false)
session.startSyncing(applicationContext)
startNextActivityAndFinish()
} catch (failure: Throwable) {
displayError(failure)
}
}
}
args.clearCredentials -> session.signOut(
!args.isUserLoggedOut,
object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.w("SIGN_OUT: success, start app")
sessionHolder.clearActiveSession()
doLocalCleanup(clearPreferences = true)
startNextActivityAndFinish()
}
override fun onFailure(failure: Throwable) {
displayError(failure)
}
})
args.clearCache -> session.clearCache(
object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
doLocalCleanup(clearPreferences = false)
session.startSyncing(applicationContext)
startNextActivityAndFinish()
}
override fun onFailure(failure: Throwable) {
displayError(failure)
}
})
}
}
@ -187,24 +187,22 @@ class MainActivity : VectorBaseActivity<FragmentLoadingBinding>(), UnlockedActiv
Timber.w("Ignoring invalid token global error")
}
private fun doLocalCleanup(clearPreferences: Boolean) {
GlobalScope.launch(Dispatchers.Main) {
// On UI Thread
Glide.get(this@MainActivity).clearMemory()
private suspend fun doLocalCleanup(clearPreferences: Boolean) {
// On UI Thread
Glide.get(this@MainActivity).clearMemory()
if (clearPreferences) {
vectorPreferences.clearPreferences()
uiStateRepository.reset()
pinLocker.unlock()
pinCodeStore.deleteEncodedPin()
}
withContext(Dispatchers.IO) {
// On BG thread
Glide.get(this@MainActivity).clearDiskCache()
if (clearPreferences) {
vectorPreferences.clearPreferences()
uiStateRepository.reset()
pinLocker.unlock()
pinCodeStore.deleteEncodedPin()
}
withContext(Dispatchers.IO) {
// On BG thread
Glide.get(this@MainActivity).clearDiskCache()
// Also clear cache (Logs, etc...)
deleteAllFiles(this@MainActivity.cacheDir)
}
// Also clear cache (Logs, etc...)
deleteAllFiles(this@MainActivity.cacheDir)
}
}

View file

@ -19,6 +19,7 @@ package im.vector.app.features.attachments.preview
import android.app.Activity.RESULT_CANCELED
import android.app.Activity.RESULT_OK
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
@ -153,8 +154,13 @@ class AttachmentsPreviewFragment @Inject constructor(
)
}
@Suppress("DEPRECATION")
private fun applyInsets() {
view?.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
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
}
ViewCompat.setOnApplyWindowInsetsListener(views.attachmentPreviewerBottomContainer) { v, insets ->
v.updatePadding(bottom = insets.systemWindowInsetBottom)
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");
* you may not use this file except in compliance with the License.
@ -14,6 +14,12 @@
* 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 savedIsMicrophoneMute = false
private var savedAudioMode = AudioManager.MODE_INVALID
private var savedAudioMode = AudioManager.MODE_NORMAL
private var connectedBlueToothHeadset: BluetoothProfile? = null
private var wantsBluetoothConnection = false

View file

@ -25,8 +25,11 @@ import android.os.Bundle
import android.os.Parcelable
import android.view.View
import android.view.Window
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.view.ViewCompat
import androidx.core.view.isInvisible
@ -102,29 +105,49 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
setContentView(R.layout.activity_call)
}
@Suppress("DEPRECATION")
private fun hideSystemUI() {
systemUiVisibility = false
// Enables regular immersive mode.
// For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE.
// Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
// Set the content to appear under the system bars so that the
// content doesn't resize when the system bars hide and show.
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
// Hide the nav bar and status bar
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN)
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
// Set the content to appear under the system bars so that the
// content doesn't resize when the system bars hide and show.
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
// Hide the nav bar and status bar
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN)
}
}
// Shows the system bars by removing all the flags
// except for the ones that make the content appear under the system bars.
@Suppress("DEPRECATION")
private fun showSystemUI() {
systemUiVisibility = true
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
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
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
}
}
private fun toggleUiSystemVisibility() {

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