diff --git a/CHANGES.md b/CHANGES.md index 135b881501..4d7eedc38c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,34 @@ -Changes in Element 1.0.14 (2020-XX-XX) +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 + +Changes in Element 1.0.14 (2020-01-15) =================================================== Features ✨: diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle index d8cd7d0c98..5ce9f1eff6 100644 --- a/attachment-viewer/build.gradle +++ b/attachment-viewer/build.gradle @@ -32,11 +32,11 @@ buildscript { } android { - compileSdkVersion 29 + compileSdkVersion 30 defaultConfig { minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 30 versionCode 1 versionName "1.0" } diff --git a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt index 9b1345cd39..418b5b5cbb 100644 --- a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt +++ b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt @@ -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) + } } } diff --git a/attachment-viewer/src/main/res/values/colors.xml b/attachment-viewer/src/main/res/values/colors.xml new file mode 100644 index 0000000000..7ceef40881 --- /dev/null +++ b/attachment-viewer/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + + #80000000 + + \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/40100150.txt b/fastlane/metadata/android/en-US/changelogs/40100150.txt new file mode 100644 index 0000000000..c3e10cefbd --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40100150.txt @@ -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 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f1577bddaa..517ae0d4ce 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=a7ca23b3ccf265680f2bfd35f1f00b1424f4466292c7337c85d46c9641b3f053 -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-all.zip +distributionSha256Sum=3db89524a3981819ff28c3f979236c1274a726e146ced0c8a2020417f9bc0782 +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/matrix-sdk-android-rx/build.gradle b/matrix-sdk-android-rx/build.gradle index a99b5856ba..0e899e21ff 100644 --- a/matrix-sdk-android-rx/build.gradle +++ b/matrix-sdk-android-rx/build.gradle @@ -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" diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 6a2f7575e5..836b49b3f2 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -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 @@ -112,7 +112,7 @@ dependencies { def lifecycle_version = '2.2.0' def arch_version = '2.1.0' def markwon_version = '3.1.0' - def daggerVersion = '2.29.1' + def daggerVersion = '2.31' def work_version = '2.4.0' def retrofit_version = '2.6.2' @@ -160,8 +160,6 @@ dependencies { // DI implementation "com.google.dagger:dagger:$daggerVersion" kapt "com.google.dagger:dagger-compiler:$daggerVersion" - compileOnly 'com.squareup.inject:assisted-inject-annotations-dagger2:0.5.0' - kapt 'com.squareup.inject:assisted-inject-processor-dagger2:0.5.0' // Logging implementation 'com.jakewharton.timber:timber:4.7.1' diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt index 9996eef0a8..b0df6fcb44 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt @@ -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) { + 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) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index cb49ee8818..a4dbd70b11 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -378,7 +378,9 @@ class CommonTestHelper(context: Context) { fun Iterable.signOutAndClose() = forEach { signOutAndClose(it) } fun signOutAndClose(session: Session) { - doSync(60_000) { session.signOut(true, it) } + runBlockingTest(timeout = 60_000) { + session.signOut(true) + } // no need signout will close // session.close() } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index 3d5856fc64..eb7e4a9fbe 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -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 { session.cryptoService().crossSigningService() - .initializeCrossSigning(UserPasswordAuth( - user = session.myUserId, - password = TestConstants.PASSWORD - ), it) + .initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume( + UserPasswordAuth( + user = session.myUserId, + password = TestConstants.PASSWORD, + session = flowResponse.session + ) + ) + } + }, it) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt index 0e3b29118c..cf31294e2f 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt @@ -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 { bobSession.cryptoService().crossSigningService() - .initializeCrossSigning(UserPasswordAuth( - user = bobSession.myUserId, - password = TestConstants.PASSWORD - ), it) + .initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume( + UserPasswordAuth( + user = bobSession.myUserId, + password = TestConstants.PASSWORD, + session = flowResponse.session + ) + ) + } + }, it) } // Wait until we received back the key diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt index 38c57bd22a..44af87bcbe 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt @@ -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 { aliceSession.cryptoService().crossSigningService() - .initializeCrossSigning(UserPasswordAuth( - user = aliceSession.myUserId, - password = TestConstants.PASSWORD - ), it) + .initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + 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 { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(aliceAuthParams, it) } - mTestHelper.doSync { bobSession.cryptoService().crossSigningService().initializeCrossSigning(bobAuthParams, it) } + mTestHelper.doSync { + aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(aliceAuthParams) + } + }, it) + } + mTestHelper.doSync { bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(bobAuthParams) + } + }, it) } // Check that alice can see bob keys mTestHelper.doSync> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) } @@ -122,8 +144,16 @@ class XSigningTest : InstrumentedTest { password = TestConstants.PASSWORD ) - mTestHelper.doSync { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(aliceAuthParams, it) } - mTestHelper.doSync { bobSession.cryptoService().crossSigningService().initializeCrossSigning(bobAuthParams, it) } + mTestHelper.doSync { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(aliceAuthParams) + } + }, it) } + mTestHelper.doSync { bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(bobAuthParams) + } + }, it) } // Check that alice can see bob keys val bobUserId = bobSession.myUserId diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt index 2c4d89b070..8c3917adc1 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt @@ -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 { aliceSession1.cryptoService().crossSigningService() - .initializeCrossSigning(UserPasswordAuth( - user = aliceSession1.myUserId, - password = TestConstants.PASSWORD - ), it) + .initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + 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 { aliceSession.cryptoService().crossSigningService() - .initializeCrossSigning(UserPasswordAuth( - user = aliceSession.myUserId, - password = TestConstants.PASSWORD - ), it) + .initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + 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 { bobSession.cryptoService().crossSigningService() - .initializeCrossSigning(UserPasswordAuth( - user = bobSession.myUserId, - password = TestConstants.PASSWORD - ), it) + .initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + 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()?.body}") assert(dRes?.clearEvent == null) } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt index 1385dac1ec..397f7f9441 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt @@ -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 { 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) { + promise.resume( + UserPasswordAuth( + user = aliceSession.myUserId, + password = TestConstants.PASSWORD, + session = flowResponse.session + ) + ) + } + }, callback) } mTestHelper.doSync { 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) { + promise.resume( + UserPasswordAuth( + user = bobSession.myUserId, + password = TestConstants.PASSWORD, + session = flowResponse.session + ) + ) + } + }, callback) } val aliceVerificationService = aliceSession.cryptoService().verificationService() diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/media/UrlsExtractorTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/media/UrlsExtractorTest.kt index 9ee84fdfc6..473b18b31b 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/media/UrlsExtractorTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/media/UrlsExtractorTest.kt @@ -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 + ) + ) + } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt index 34edf37733..f156a5eb64 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt @@ -66,8 +66,8 @@ class TimelineForwardPaginationTest : InstrumentedTest { numberOfMessagesToSend) // Alice clear the cache - commonTestHelper.doSync { - aliceSession.clearCache(it) + commonTestHelper.runBlockingTest { + aliceSession.clearCache() } // And restarts the sync diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/TokenBasedAuth.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/TokenBasedAuth.kt new file mode 100644 index 0000000000..e522352c38 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/TokenBasedAuth.kt @@ -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 = mapOf( + "session" to session, + "token" to token, + "transactionId" to transactionId, + "type" to type + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SessionAssistedInjectModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UIABaseAuth.kt similarity index 55% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SessionAssistedInjectModule.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UIABaseAuth.kt index da3ecfb907..d5e323e457 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SessionAssistedInjectModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UIABaseAuth.kt @@ -5,7 +5,7 @@ * 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 + * 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, @@ -14,11 +14,18 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.di +package org.matrix.android.sdk.api.auth -import com.squareup.inject.assisted.dagger2.AssistedModule -import dagger.Module +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? -@AssistedModule -@Module(includes = [AssistedInject_SessionAssistedInjectModule::class]) -interface SessionAssistedInjectModule + fun hasAuthInfo(): Boolean + + fun copyWithSession(session: String): UIABaseAuth + + fun asMap() : Map +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserInteractiveAuthInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserInteractiveAuthInterceptor.kt new file mode 100644 index 0000000000..16a5c8073d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserInteractiveAuthInterceptor.kt @@ -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) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UserPasswordAuth.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserPasswordAuth.kt similarity index 74% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UserPasswordAuth.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserPasswordAuth.kt index ba8b34096c..e985c5f08a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UserPasswordAuth.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserPasswordAuth.kt @@ -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 = mapOf( + "session" to session, + "user" to user, + "password" to password, + "type" to type + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt index 6759c59237..cfaf74ce24 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt @@ -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" } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationFlowResponse.kt similarity index 93% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationFlowResponse.kt index 3461a4d738..2b1c1c09b3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationFlowResponse.kt @@ -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() } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt index 4711f7957d..c06cdd9e23 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt @@ -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 } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt index de881b9e02..b241903364 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt index 895be0031a..3820a442aa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt @@ -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? = null, + @Json(name = "flows") + val flows: List? = null, + @Json(name = "params") + val params: JsonDict? = null ) { companion object { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index 8a95baf3cb..ff7c9f0521 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -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 */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt index 8915202f35..eb327dfd56 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt @@ -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) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/cache/CacheService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/cache/CacheService.kt index c1c5663227..2945cc45d6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/cache/CacheService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/cache/CacheService.kt @@ -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) + suspend fun clearCache() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt index 0eefca1b4c..fa5ea359e8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -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) - fun deleteDevice(deviceId: String, callback: MatrixCallback) + fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt index 6a646cd4c7..359e33cc2c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt @@ -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) fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt index 9040ec7d5c..3b3ef57d73 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt @@ -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 + fun extractUrls(event: TimelineEvent): List /** * Get Raw Url Preview data from the homeserver. There is no cache management for this request diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt index 1fd8360253..a4d5b665c6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt @@ -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): Cancelable /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 73cb94b417..b10fb540e1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -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 */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/signout/SignOutService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/signout/SignOutService.kt index ebbbac527a..4e4eba274e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/signout/SignOutService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/signout/SignOutService.kt @@ -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): Cancelable + suspend fun signInAgain(password: String) /** * Update the session with credentials received after SSO */ - fun updateCredentials(credentials: Credentials, - callback: MatrixCallback): 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): Cancelable + suspend fun signOut(signOutFromHomeserver: Boolean) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/Constants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/Constants.kt index 642279cc27..e0c52cf9ca 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/Constants.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/Constants.kt @@ -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" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt index 2b26115f30..d0d17e2cd5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt @@ -43,5 +43,6 @@ internal data class LoginFlow( * See MSC #2858 */ @Json(name = "org.matrix.msc2858.identity_providers") - val ssoIdentityProvider: List? + val ssoIdentityProvider: List? = null + ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt index 9c6b942a4f..163009d918 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/UIAExt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/UIAExt.kt new file mode 100644 index 0000000000..1a0383cb22 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/UIAExt.kt @@ -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 { 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) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index ebd809f777..678bc9819f 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -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) { + override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) { deleteDeviceTask - .configureWith(DeleteDeviceTask.Params(deviceId)) { + .configureWith(DeleteDeviceTask.Params(deviceId, userInteractiveAuthInterceptor, null)) { this.executionThread = TaskThread.CRYPTO this.callback = callback } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt index bcad448eb6..9b282f0a84 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -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) { + override fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?, callback: MatrixCallback) { 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) { - 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() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt index 665d770e7f..1660bae0b7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt @@ -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 + // Kept for compatibility, but not used anymore (can be used for pending Worker) + val updatedUserIds: List? = 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) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorkerDataRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorkerDataRepository.kt new file mode 100644 index 0000000000..0878a9f765 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorkerDataRepository.kt @@ -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 +) + +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 { + 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() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DefaultBaseAuth.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DefaultBaseAuth.kt new file mode 100644 index 0000000000..bbb4a3a654 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DefaultBaseAuth.kt @@ -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 = mapOf("session" to session) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt index 0ce6f1f41c..f636ab890d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt @@ -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? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSigningKeysBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSigningKeysBody.kt index 3418bb327d..d24b7ae5f0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSigningKeysBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSigningKeysBody.kt @@ -30,5 +30,5 @@ internal data class UploadSigningKeysBody( val userSigningKey: RestKeyInfo? = null, @Json(name = "auth") - val auth: UserPasswordAuth? = null + val auth: Map? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt index 8f1569a037..ff25ac0f66 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt @@ -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 { 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(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 + } } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt index b4c1e6d27c..dc0077425e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt @@ -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() ) ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt index 6c0a76fa7d..ef31130f55 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt @@ -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 { 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() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt index cceff355bb..14fad2ea38 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt @@ -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) { + 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") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt index 659fcc8f5c..f5eade1704 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt @@ -27,7 +27,6 @@ import org.matrix.android.sdk.internal.crypto.SendGossipWorker import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker import org.matrix.android.sdk.internal.di.MatrixComponent -import org.matrix.android.sdk.internal.di.SessionAssistedInjectModule import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker import org.matrix.android.sdk.internal.session.account.AccountModule import org.matrix.android.sdk.internal.session.cache.CacheModule @@ -86,7 +85,6 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers TermsModule::class, AccountDataModule::class, ProfileModule::class, - SessionAssistedInjectModule::class, AccountModule::class, CallModule::class, SearchModule::class diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordParams.kt index 70cdbda399..1b95820918 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordParams.kt @@ -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. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountParams.kt index 6c2e8b4a4e..d9b4c748b9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountParams.kt @@ -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? = 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 ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt index 9fb1cbb7d7..d67b21567e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt @@ -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 { 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(globalErrorReceiver) { - apiCall = accountAPI.deactivate(deactivateAccountParams) + try { + executeRequest(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") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt index 1165d2116b..25b67159a9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt @@ -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)) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt index 19365fce0a..6d0cd37e1f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt @@ -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) { + override suspend fun clearCache() { taskExecutor.cancelAll() - clearCacheTask - .configureWith { - this.callback = callback - } - .executeBy(taskExecutor) + clearCacheTask.execute(Unit) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt index 6cf65b867c..1d6cd61060 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt @@ -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) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt index 4b31db59b1..c28668a53e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt @@ -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 { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 672d407d25..3b727690bf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -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 { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt index a03bef9501..67f3b2aa56 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt @@ -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 - return handleSuccess(params.threePids, hashedAddresses, identityLookUpV2Response) + return handleSuccess(params.threePids, lookUpData) } + data class LookUpData( + val hashedAddresses: List, + val identityLookUpResponse: IdentityLookUpResponse + ) + private suspend fun lookUpInternal(identityAPI: IdentityAPI, - hashedAddresses: List, + threePids: List, 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(null) { - apiCall = identityAPI.hashDetails() + private fun getHashedAddresses(threePids: List, pepper: String): List { + 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, hashedAddresses: List, identityLookUpResponse: IdentityLookUpResponse): List { - 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, lookupData: LookUpData): List { + return lookupData.identityLookUpResponse.mappings.keys.map { hashedAddress -> + FoundThreePid( + threePids[lookupData.hashedAddresses.indexOf(hashedAddress)], + lookupData.identityLookUpResponse.mappings[hashedAddress] ?: error("") + ) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt index 1a400ccfcf..9b807d03de 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt @@ -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>(1_000) - override fun extractUrls(event: Event): List { + override fun extractUrls(event: TimelineEvent): List { 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)) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt index e531d6af9f..6137b4152c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt @@ -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 { - return event.takeIf { it.getClearType() == EventType.MESSAGE } - ?.getClearContent() - ?.toModel() + fun extract(event: TimelineEvent): List { + return event.takeIf { it.root.getClearType() == EventType.MESSAGE } + ?.getLastMessageContent() ?.takeIf { it.msgType == MessageType.MSGTYPE_TEXT || it.msgType == MessageType.MSGTYPE_NOTICE diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt index 500d43408e..b3216d744d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt @@ -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): 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) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddThreePidBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddThreePidBody.kt index 4e46dd096d..6301929545 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddThreePidBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddThreePidBody.kt @@ -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? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt index 1e3a2cb501..916a602936 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt @@ -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 { 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 + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/DefaultAliasService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/DefaultAliasService.kt index b6c69224e6..8f58094a2a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/DefaultAliasService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/DefaultAliasService.kt @@ -16,8 +16,9 @@ package org.matrix.android.sdk.internal.session.room.alias -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import org.matrix.android.sdk.api.session.room.alias.AliasService internal class DefaultAliasService @AssistedInject constructor( @@ -26,9 +27,9 @@ internal class DefaultAliasService @AssistedInject constructor( private val addRoomAliasTask: AddRoomAliasTask ) : AliasService { - @AssistedInject.Factory + @AssistedFactory interface Factory { - fun create(roomId: String): AliasService + fun create(roomId: String): DefaultAliasService } override suspend fun getRoomAliases(): List { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/call/DefaultRoomCallService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/call/DefaultRoomCallService.kt index 205a085df6..9bde5054f6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/call/DefaultRoomCallService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/call/DefaultRoomCallService.kt @@ -16,8 +16,9 @@ package org.matrix.android.sdk.internal.session.room.call -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.call.RoomCallService import org.matrix.android.sdk.internal.session.room.RoomGetter @@ -27,9 +28,9 @@ internal class DefaultRoomCallService @AssistedInject constructor( private val roomGetter: RoomGetter ) : RoomCallService { - @AssistedInject.Factory + @AssistedFactory interface Factory { - fun create(roomId: String): RoomCallService + fun create(roomId: String): DefaultRoomCallService } override fun canStartCall(): Boolean { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt index fb840b4eb3..5e823fc87f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt @@ -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) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt index 93fbfb4df0..1d4ab6d516 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt @@ -17,8 +17,9 @@ package org.matrix.android.sdk.internal.session.room.draft import androidx.lifecycle.LiveData -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.session.room.send.DraftService import org.matrix.android.sdk.api.session.room.send.UserDraft @@ -30,9 +31,9 @@ internal class DefaultDraftService @AssistedInject constructor(@Assisted private private val coroutineDispatchers: MatrixCoroutineDispatchers ) : DraftService { - @AssistedInject.Factory + @AssistedFactory interface Factory { - fun create(roomId: String): DraftService + fun create(roomId: String): DefaultDraftService } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt index 4fc865304b..cd1c9bbbdd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt @@ -17,8 +17,9 @@ package org.matrix.android.sdk.internal.session.room.membership import androidx.lifecycle.LiveData -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.identity.ThreePid @@ -58,9 +59,9 @@ internal class DefaultMembershipService @AssistedInject constructor( private val userId: String ) : MembershipService { - @AssistedInject.Factory + @AssistedFactory interface Factory { - fun create(roomId: String): MembershipService + fun create(roomId: String): DefaultMembershipService } override fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback): Cancelable { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt index 67ae55c066..5486d96e28 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/DefaultRoomPushRuleService.kt @@ -18,8 +18,9 @@ package org.matrix.android.sdk.internal.session.room.notification import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.pushrules.RuleScope import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState @@ -33,9 +34,9 @@ internal class DefaultRoomPushRuleService @AssistedInject constructor(@Assisted @SessionDatabase private val monarchy: Monarchy) : RoomPushRuleService { - @AssistedInject.Factory + @AssistedFactory interface Factory { - fun create(roomId: String): RoomPushRuleService + fun create(roomId: String): DefaultRoomPushRuleService } override fun getLiveRoomNotificationState(): LiveData { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt index 025bea09f4..3cf8cfe5ee 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt @@ -18,8 +18,9 @@ package org.matrix.android.sdk.internal.session.room.read import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.room.model.ReadReceipt @@ -46,9 +47,9 @@ internal class DefaultReadService @AssistedInject constructor( @UserId private val userId: String ) : ReadService { - @AssistedInject.Factory + @AssistedFactory interface Factory { - fun create(roomId: String): ReadService + fun create(roomId: String): DefaultReadService } override fun markAsRead(params: ReadService.MarkAsReadParams, callback: MatrixCallback) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt index a7f3f83980..b7caf62865 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt @@ -17,8 +17,9 @@ package org.matrix.android.sdk.internal.session.room.relation import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.events.model.Event @@ -56,9 +57,9 @@ internal class DefaultRelationService @AssistedInject constructor( private val taskExecutor: TaskExecutor) : RelationService { - @AssistedInject.Factory + @AssistedFactory interface Factory { - fun create(roomId: String): RelationService + fun create(roomId: String): DefaultRelationService } override fun sendReaction(targetEventId: String, reaction: String): Cancelable { @@ -140,7 +141,7 @@ internal class DefaultRelationService @AssistedInject constructor( } override fun fetchEditHistory(eventId: String, callback: MatrixCallback>) { - val params = FetchEditHistoryTask.Params(roomId, cryptoSessionInfoProvider.isRoomEncrypted(roomId), eventId) + val params = FetchEditHistoryTask.Params(roomId, eventId) fetchEditHistoryTask .configureWith(params) { this.callback = callback diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt index 99d02b50da..854585ca29 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt @@ -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> { - 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 { + val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId) val response = executeRequest(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() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt index cac87a9d30..add17a9fa5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt @@ -16,17 +16,18 @@ package org.matrix.android.sdk.internal.session.room.reporting -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import org.matrix.android.sdk.api.session.room.reporting.ReportingService internal class DefaultReportingService @AssistedInject constructor(@Assisted private val roomId: String, private val reportContentTask: ReportContentTask ) : ReportingService { - @AssistedInject.Factory + @AssistedFactory interface Factory { - fun create(roomId: String): ReportingService + fun create(roomId: String): DefaultReportingService } override suspend fun reportContent(eventId: String, score: Int, reason: String) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index 8828f3dfed..a12962b51f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -21,8 +21,9 @@ import androidx.work.BackoffPolicy import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequest import androidx.work.Operation -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.events.model.Event @@ -71,9 +72,9 @@ internal class DefaultSendService @AssistedInject constructor( private val cancelSendTracker: CancelSendTracker ) : SendService { - @AssistedInject.Factory + @AssistedFactory interface Factory { - fun create(roomId: String): SendService + fun create(roomId: String): DefaultSendService } private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt index 804968bac0..f2640fd1e7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt @@ -18,8 +18,9 @@ package org.matrix.android.sdk.internal.session.room.state import android.net.Uri import androidx.lifecycle.LiveData -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType @@ -42,9 +43,9 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private private val fileUploader: FileUploader ) : StateService { - @AssistedInject.Factory + @AssistedFactory interface Factory { - fun create(roomId: String): StateService + fun create(roomId: String): DefaultStateService } override fun getStateEvent(eventType: String, stateKey: QueryStringValue): Event? { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index 8c71604183..fff780fb0c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -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) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt index d6c02f0a49..02acaa0570 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tags/DefaultTagsService.kt @@ -16,8 +16,9 @@ package org.matrix.android.sdk.internal.session.room.tags -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import org.matrix.android.sdk.api.session.room.tags.TagsService internal class DefaultTagsService @AssistedInject constructor( @@ -26,9 +27,9 @@ internal class DefaultTagsService @AssistedInject constructor( private val deleteTagFromRoomTask: DeleteTagFromRoomTask ) : TagsService { - @AssistedInject.Factory + @AssistedFactory interface Factory { - fun create(roomId: String): TagsService + fun create(roomId: String): DefaultTagsService } override suspend fun addTag(tag: String, order: Double?) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index fce09cc97c..ef890db79e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -18,8 +18,9 @@ package org.matrix.android.sdk.internal.session.room.timeline import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import com.zhuinden.monarchy.Monarchy import io.realm.Sort import io.realm.kotlin.where @@ -55,9 +56,9 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv private val loadRoomMembersTask: LoadRoomMembersTask ) : TimelineService { - @AssistedInject.Factory + @AssistedFactory interface Factory { - fun create(roomId: String): TimelineService + fun create(roomId: String): DefaultTimelineService } override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/EventContextResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/EventContextResponse.kt index bce03354d7..654cf0fb74 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/EventContextResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/EventContextResponse.kt @@ -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 = emptyList(), - @Json(name = "events_after") val eventsAfter: List = emptyList(), + /** + * A list of room events that happened just before the requested event, in reverse-chronological order. + */ + @Json(name = "events_before") val eventsBefore: List? = null, + /** + * A list of room events that happened just after the requested event, in chronological order. + */ + @Json(name = "events_after") val eventsAfter: List? = 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 = emptyList() + /** + * The state of the room at the last event returned. + */ + @Json(name = "state") override val stateEvents: List? = null ) : TokenChunkEvent { override val events: List by lazy { - eventsAfter.reversed() + listOf(event) + eventsBefore + eventsAfter.orEmpty().reversed() + event + eventsBefore.orEmpty() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationResponse.kt index ed384d3b3c..2f61b1cce8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationResponse.kt @@ -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 = emptyList(), - @Json(name = "state") override val stateEvents: List = 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? = 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? = null +) : TokenChunkEvent { + override val events: List + get() = chunk.orEmpty() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEvent.kt index 08b20f1701..465b0faac8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEvent.kt @@ -22,7 +22,7 @@ internal interface TokenChunkEvent { val start: String? val end: String? val events: List - val stateEvents: List + val stateEvents: List? fun hasMore() = start != end } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt index 2a532c6bf5..1a497b8835 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -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(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) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/DefaultTypingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/DefaultTypingService.kt index 5dcf3fcdd6..39b7967bc1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/DefaultTypingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/DefaultTypingService.kt @@ -17,8 +17,9 @@ package org.matrix.android.sdk.internal.session.room.typing import android.os.SystemClock -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.room.typing.TypingService import org.matrix.android.sdk.api.util.Cancelable @@ -38,9 +39,9 @@ internal class DefaultTypingService @AssistedInject constructor( private val sendTypingTask: SendTypingTask ) : TypingService { - @AssistedInject.Factory + @AssistedFactory interface Factory { - fun create(roomId: String): TypingService + fun create(roomId: String): DefaultTypingService } private var currentTask: Cancelable? = null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt index 895f1cf50d..6d841644dc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/DefaultUploadsService.kt @@ -16,8 +16,9 @@ package org.matrix.android.sdk.internal.session.room.uploads -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.room.uploads.GetUploadsResult import org.matrix.android.sdk.api.session.room.uploads.UploadsService @@ -28,9 +29,9 @@ internal class DefaultUploadsService @AssistedInject constructor( private val cryptoService: CryptoService ) : UploadsService { - @AssistedInject.Factory + @AssistedFactory interface Factory { - fun create(roomId: String): UploadsService + fun create(roomId: String): DefaultUploadsService } override suspend fun getUploads(numberOfEvents: Int, since: String?): GetUploadsResult { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt index 0c0e6a8ed0..b3e4a5aa05 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/DefaultSignOutService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/DefaultSignOutService.kt index ea3730b195..e7b20f905b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/DefaultSignOutService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/DefaultSignOutService.kt @@ -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): 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): 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): Cancelable { - return signOutTask - .configureWith(SignOutTask.Params(signOutFromHomeserver)) { - this.callback = callback - } - .executeBy(taskExecutor) + override suspend fun signOut(signOutFromHomeserver: Boolean) { + return signOutTask.execute(SignOutTask.Params(signOutFromHomeserver)) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt index c41f1df0de..000b9e38b9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt @@ -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) diff --git a/multipicker/build.gradle b/multipicker/build.gradle index c58c4586b2..10dc18e488 100644 --- a/multipicker/build.gradle +++ b/multipicker/build.gradle @@ -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" diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/AudioPicker.kt b/multipicker/src/main/java/im/vector/lib/multipicker/AudioPicker.kt index 516022100d..e8970d72ef 100644 --- a/multipicker/src/main/java/im/vector/lib/multipicker/AudioPicker.kt +++ b/multipicker/src/main/java/im/vector/lib/multipicker/AudioPicker.kt @@ -58,7 +58,7 @@ class AudioPicker : Picker() { 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( diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/VideoPicker.kt b/multipicker/src/main/java/im/vector/lib/multipicker/VideoPicker.kt index c7c06f795f..dada9ac5bd 100644 --- a/multipicker/src/main/java/im/vector/lib/multipicker/VideoPicker.kt +++ b/multipicker/src/main/java/im/vector/lib/multipicker/VideoPicker.kt @@ -61,10 +61,10 @@ class VideoPicker : Picker() { 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( diff --git a/tools/templates/ElementFeature/root/src/app_package/ViewModel.kt.ftl b/tools/templates/ElementFeature/root/src/app_package/ViewModel.kt.ftl index d3c50c961c..8c25f9f9a8 100644 --- a/tools/templates/ElementFeature/root/src/app_package/ViewModel.kt.ftl +++ b/tools/templates/ElementFeature/root/src/app_package/ViewModel.kt.ftl @@ -4,8 +4,9 @@ import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel @@ -21,7 +22,7 @@ class ${viewModelClass} @AssistedInject constructor(@Assisted initialState: ${vi : VectorViewModel<${viewStateClass}, ${actionClass}, EmptyViewEvents>(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: ${viewStateClass}): ${viewModelClass} } diff --git a/vector/build.gradle b/vector/build.gradle index f6ba5d6e27..ced0899cf9 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -13,7 +13,7 @@ kapt { // Note: 2 digits max for each value ext.versionMajor = 1 ext.versionMinor = 0 -ext.versionPatch = 14 +ext.versionPatch = 15 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. @@ -291,7 +291,7 @@ dependencies { def big_image_viewer_version = '1.6.2' def glide_version = '4.11.0' def moshi_version = '1.11.0' - def daggerVersion = '2.29.1' + def daggerVersion = '2.31' def autofill_version = "1.0.0" def work_version = '2.4.0' def arch_version = '2.1.0' @@ -412,8 +412,6 @@ dependencies { // DI implementation "com.google.dagger:dagger:$daggerVersion" kapt "com.google.dagger:dagger-compiler:$daggerVersion" - compileOnly 'com.squareup.inject:assisted-inject-annotations-dagger2:0.5.0' - kapt 'com.squareup.inject:assisted-inject-processor-dagger2:0.5.0' // gplay flavor only gplayImplementation('com.google.firebase:firebase-messaging:21.0.0') { diff --git a/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt b/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt index d9005e4a63..571bcf474c 100644 --- a/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt +++ b/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt @@ -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 { existingSession!!.cryptoService().crossSigningService() - .initializeCrossSigning(UserPasswordAuth( - user = existingSession!!.myUserId, - password = "password" - ), it) + .initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume( + UserPasswordAuth( + user = existingSession!!.myUserId, + password = "password", + session = flowResponse.session + ) + ) + } + }, it) } } diff --git a/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt b/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt index 8a21260ac7..c51ff29669 100644 --- a/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt +++ b/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt @@ -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 { existingSession!!.cryptoService().crossSigningService() - .initializeCrossSigning(UserPasswordAuth( - user = existingSession!!.myUserId, - password = "password" - ), it) + .initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + 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) { + promise.resume( + UserPasswordAuth( + user = existingSession!!.myUserId, + password = password, + session = flowResponse.session + ) + ) + } + }, passphrase = passphrase, setupMode = SetupMode.NORMAL )) diff --git a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt index 58b596b05f..2d0077fc55 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt @@ -196,6 +196,8 @@ class UiAllScreensSanityTest { pressBack() clickMenu(R.id.video_call) pressBack() + clickMenu(R.id.search) + pressBack() pressBack() } diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 0341059674..bfaea39cc6 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -63,7 +63,6 @@ @@ -242,6 +241,27 @@ + + + + + + + + + + + + + + + + + // 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) diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt index a585e8ea77..d8b61f3cba 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt @@ -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 : 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) + } } /* ========================================================================================== diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt index 6f41a6a846..0b951fb5a2 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt @@ -200,6 +200,7 @@ abstract class VectorBaseFragment : 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)) diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericPositiveButtonItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericPositiveButtonItem.kt new file mode 100644 index 0000000000..d18adde4ba --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericPositiveButtonItem.kt @@ -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() { + + @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(R.id.itemGenericItemButton) + } +} diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt index cb7be9ee3b..351163b026 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -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(), 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 { - 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 { - 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(), 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) } } diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt index 407b51666b..cdb015e4da 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt b/vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt new file mode 100644 index 0000000000..917f60dacb --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt @@ -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() { + + 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 + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/di/AssistedInjectModule.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthActions.kt similarity index 51% rename from vector/src/main/java/im/vector/app/core/di/AssistedInjectModule.kt rename to vector/src/main/java/im/vector/app/features/auth/ReAuthActions.kt index 0ba4bedb92..036afda405 100644 --- a/vector/src/main/java/im/vector/app/core/di/AssistedInjectModule.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthActions.kt @@ -1,11 +1,11 @@ /* - * Copyright 2019 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. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -14,11 +14,14 @@ * limitations under the License. */ -package im.vector.app.core.di +package im.vector.app.features.auth -import com.squareup.inject.assisted.dagger2.AssistedModule -import dagger.Module +import im.vector.app.core.platform.VectorViewModelAction -@AssistedModule -@Module(includes = [AssistedInject_AssistedInjectModule::class]) -interface AssistedInjectModule +sealed class ReAuthActions : VectorViewModelAction { + object StartSSOFallback : ReAuthActions() + object FallBackPageLoaded : ReAuthActions() + object FallBackPageClosed : ReAuthActions() + object TogglePassVisibility : ReAuthActions() + data class ReAuthWithPass(val password: String) : ReAuthActions() +} diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt new file mode 100644 index 0000000000..0385973386 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt @@ -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(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)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/error/SsoFlowNotSupportedYet.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthEvents.kt similarity index 61% rename from vector/src/main/java/im/vector/app/core/error/SsoFlowNotSupportedYet.kt rename to vector/src/main/java/im/vector/app/features/auth/ReAuthEvents.kt index 7b22072c34..8cf9be6fb1 100644 --- a/vector/src/main/java/im/vector/app/core/error/SsoFlowNotSupportedYet.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthEvents.kt @@ -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() +} diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt new file mode 100644 index 0000000000..540a08405c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt @@ -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) +} diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt new file mode 100644 index 0000000000..4204da0d24 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt @@ -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(initialState) { + + @AssistedFactory + interface Factory { + fun create(initialState: ReAuthState): ReAuthViewModel + } + + companion object : MvRxViewModelFactory { + + 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)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt index de8003b28b..b2ad0a7688 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt @@ -18,8 +18,9 @@ package im.vector.app.features.autocomplete.member import android.content.Context import androidx.recyclerview.widget.RecyclerView -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.features.autocomplete.AutocompleteClickListener import im.vector.app.features.autocomplete.RecyclerViewPresenter import org.matrix.android.sdk.api.query.QueryStringValue @@ -44,7 +45,7 @@ class AutocompleteMemberPresenter @AssistedInject constructor(context: Context, controller.listener = null } - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(roomId: String): AutocompleteMemberPresenter } diff --git a/vector/src/main/java/im/vector/app/features/call/CallAudioManager.kt b/vector/src/main/java/im/vector/app/features/call/CallAudioManager.kt index 3a24cf6d48..82bbaf1d54 100644 --- a/vector/src/main/java/im/vector/app/features/call/CallAudioManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/CallAudioManager.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index 41bf7bbeaf..6c49d4d3e2 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -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(), 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() { diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index bd16adf3e7..fd735de085 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -22,8 +22,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import org.matrix.android.sdk.api.MatrixCallback @@ -242,7 +243,7 @@ class VectorCallViewModel @AssistedInject constructor( }.exhaustive } - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: VectorCallViewState, args: CallArgs): VectorCallViewModel } diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt index 783b519706..97ee41154a 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt @@ -21,8 +21,9 @@ import com.airbnb.mvrx.Fail import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import org.jitsi.meet.sdk.JitsiMeetUserInfo @@ -40,7 +41,7 @@ class JitsiCallViewModel @AssistedInject constructor( private val stringProvider: StringProvider ) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: JitsiCallViewState, args: VectorJitsiActivity.Args): JitsiCallViewModel } diff --git a/vector/src/main/java/im/vector/app/features/call/telecom/CallConnection.kt b/vector/src/main/java/im/vector/app/features/call/telecom/CallConnection.kt index 6f69b4b0d0..0a9a164993 100644 --- a/vector/src/main/java/im/vector/app/features/call/telecom/CallConnection.kt +++ b/vector/src/main/java/im/vector/app/features/call/telecom/CallConnection.kt @@ -21,7 +21,6 @@ import android.os.Build import android.telecom.Connection import android.telecom.DisconnectCause import androidx.annotation.RequiresApi -import im.vector.app.features.call.VectorCallViewModel import im.vector.app.features.call.WebRtcPeerConnectionManager import timber.log.Timber import javax.inject.Inject @@ -33,7 +32,6 @@ import javax.inject.Inject ) : Connection() { @Inject lateinit var peerConnectionManager: WebRtcPeerConnectionManager - @Inject lateinit var callViewModel: VectorCallViewModel init { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt index 2c4c5d0596..d73794f8d8 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt @@ -24,8 +24,9 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.contacts.ContactsDataSource import im.vector.app.core.contacts.MappedContact import im.vector.app.core.extensions.exhaustive @@ -48,7 +49,7 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted private val session: Session) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: ContactsBookViewState): ContactsBookViewModel } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt index d074c93587..30bbedf7ec 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt @@ -21,8 +21,9 @@ import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.raw.wellknown.getElementWellknown @@ -41,7 +42,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted val session: Session) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: CreateDirectRoomViewState): CreateDirectRoomViewModel } diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt index 3666dbce8c..cb8a6ce4e9 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt @@ -22,8 +22,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import org.matrix.android.sdk.api.MatrixCallback @@ -39,7 +40,7 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS ) : VectorViewModel(initialState), KeysBackupStateListener { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: KeysBackupSettingViewState): KeysBackupSettingsViewModel } diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt index c7533cd3df..e95f250dd3 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt @@ -26,8 +26,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel @@ -71,7 +72,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor( private val session: Session) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: SharedSecureStorageViewState, args: SharedSecureStorageActivity.Args): SharedSecureStorageViewModel } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapAccountPasswordFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapAccountPasswordFragment.kt deleted file mode 100644 index feea484f06..0000000000 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapAccountPasswordFragment.kt +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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.features.crypto.recover - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import androidx.core.text.toSpannable -import com.airbnb.mvrx.parentFragmentViewModel -import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.widget.editorActionEvents -import com.jakewharton.rxbinding3.widget.textChanges -import im.vector.app.R -import im.vector.app.core.extensions.hideKeyboard -import im.vector.app.core.extensions.showPassword -import im.vector.app.core.platform.VectorBaseFragment -import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.utils.colorizeMatchingText -import im.vector.app.databinding.FragmentBootstrapEnterAccountPasswordBinding -import io.reactivex.android.schedulers.AndroidSchedulers - -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -class BootstrapAccountPasswordFragment @Inject constructor( - private val colorProvider: ColorProvider -) : VectorBaseFragment() { - - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentBootstrapEnterAccountPasswordBinding { - return FragmentBootstrapEnterAccountPasswordBinding.inflate(inflater, container, false) - } - - val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val recPassPhrase = getString(R.string.account_password) - views.bootstrapDescriptionText.text = getString(R.string.enter_account_password, recPassPhrase) - .toSpannable() - .colorizeMatchingText(recPassPhrase, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) - - views.bootstrapAccountPasswordEditText.hint = getString(R.string.account_password) - - views.bootstrapAccountPasswordEditText.editorActionEvents() - .throttleFirst(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - if (it.actionId == EditorInfo.IME_ACTION_DONE) { - submit() - } - } - .disposeOnDestroyView() - - views.bootstrapAccountPasswordEditText.textChanges() - .distinctUntilChanged() - .subscribe { - if (!it.isNullOrBlank()) { - views.bootstrapAccountPasswordTil.error = null - } - } - .disposeOnDestroyView() - - views.ssssViewShowPassword.debouncedClicks { sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) } - views.bootstrapPasswordButton.debouncedClicks { submit() } - - withState(sharedViewModel) { state -> - (state.step as? BootstrapStep.AccountPassword)?.failure?.let { - views.bootstrapAccountPasswordTil.error = it - } - } - } - - private fun submit() = withState(sharedViewModel) { state -> - if (state.step !is BootstrapStep.AccountPassword) { - return@withState - } - val accountPassword = views.bootstrapAccountPasswordEditText.text?.toString() - if (accountPassword.isNullOrBlank()) { - views.bootstrapAccountPasswordTil.error = getString(R.string.error_empty_field_your_password) - } else { - view?.hideKeyboard() - sharedViewModel.handle(BootstrapActions.ReAuth(accountPassword)) - } - } - - override fun invalidate() = withState(sharedViewModel) { state -> - if (state.step is BootstrapStep.AccountPassword) { - val isPasswordVisible = state.step.isPasswordVisible - views.bootstrapAccountPasswordEditText.showPassword(isPasswordVisible, updateCursor = false) - views.ssssViewShowPassword.setImageResource(if (isPasswordVisible) R.drawable.ic_eye_closed else R.drawable.ic_eye) - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt index 0785290d2a..ce06fe726f 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt @@ -37,7 +37,7 @@ sealed class BootstrapActions : VectorViewModelAction { object TogglePasswordVisibility : BootstrapActions() data class UpdateCandidatePassphrase(val pass: String) : BootstrapActions() data class UpdateConfirmCandidatePassphrase(val pass: String) : BootstrapActions() - data class ReAuth(val pass: String) : BootstrapActions() +// data class ReAuth(val pass: String) : BootstrapActions() object RecoveryKeySaved : BootstrapActions() object Completed : BootstrapActions() object SaveReqQueryStarted : BootstrapActions() @@ -47,4 +47,8 @@ sealed class BootstrapActions : VectorViewModelAction { object HandleForgotBackupPassphrase : BootstrapActions() data class DoMigrateWithPassphrase(val passphrase: String) : BootstrapActions() data class DoMigrateWithRecoveryKey(val recoveryKey: String) : BootstrapActions() + + object SsoAuthDone: BootstrapActions() + data class PasswordAuthDone(val password: String): BootstrapActions() + object ReAuthCancelled: BootstrapActions() } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt index f1ea50c9bf..5cc86fdf15 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt @@ -16,7 +16,9 @@ package im.vector.app.features.crypto.recover +import android.app.Activity import android.app.Dialog +import android.os.Build import android.os.Bundle import android.os.Parcelable import android.view.KeyEvent @@ -35,9 +37,12 @@ import im.vector.app.R import im.vector.app.core.di.ScreenComponent import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.databinding.BottomSheetBootstrapBinding +import im.vector.app.features.auth.ReAuthActivity import kotlinx.parcelize.Parcelize +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import javax.inject.Inject import kotlin.reflect.KClass @@ -63,6 +68,25 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment + if (activityResult.resultCode == Activity.RESULT_OK) { + when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) { + LoginFlowTypes.SSO -> { + viewModel.handle(BootstrapActions.SsoAuthDone) + } + LoginFlowTypes.PASSWORD -> { + val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: "" + viewModel.handle(BootstrapActions.PasswordAuthDone(password)) + } + else -> { + viewModel.handle(BootstrapActions.ReAuthCancelled) + } + } + } else { + viewModel.handle(BootstrapActions.ReAuthCancelled) + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.observeViewEvents { event -> @@ -84,6 +108,14 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment { promptSkip() } + is BootstrapViewEvents.RequestReAuth -> { + ReAuthActivity.newIntent(requireContext(), + event.flowResponse, + event.lastErrorCode, + getString(R.string.initialize_cross_signing)).let { intent -> + reAuthActivityResultLauncher.launch(intent) + } + } } } } @@ -102,7 +134,12 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment= Build.VERSION_CODES.R) { + dialog?.window?.setDecorFitsSystemWindows(false) + } else { + @Suppress("DEPRECATION") + dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + } return rootView } @@ -143,11 +180,11 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment { + is BootstrapStep.AccountReAuth -> { views.bootstrapIcon.isVisible = true views.bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_user)) - views.bootstrapTitleText.text = getString(R.string.account_password) - showFragment(BootstrapAccountPasswordFragment::class, Bundle()) + views.bootstrapTitleText.text = getString(R.string.re_authentication_activity_title) + showFragment(BootstrapReAuthFragment::class, Bundle()) } is BootstrapStep.Initializing -> { views.bootstrapIcon.isVisible = true diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt index 47e373ed0a..d1a1237463 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -20,10 +20,9 @@ import im.vector.app.R import im.vector.app.core.platform.ViewModelTask import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.resources.StringProvider -import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError -import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME @@ -38,7 +37,6 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreat import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey -import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth import org.matrix.android.sdk.internal.util.awaitCallback import timber.log.Timber import java.util.UUID @@ -51,16 +49,12 @@ sealed class BootstrapResult { abstract class Failure(val error: String?) : BootstrapResult() - class UnsupportedAuthFlow : Failure(null) - data class GenericError(val failure: Throwable) : Failure(failure.localizedMessage) data class InvalidPasswordError(val matrixError: MatrixError) : Failure(null) class FailedToCreateSSSSKey(failure: Throwable) : Failure(failure.localizedMessage) class FailedToSetDefaultSSSSKey(failure: Throwable) : Failure(failure.localizedMessage) class FailedToStorePrivateKeyInSSSS(failure: Throwable) : Failure(failure.localizedMessage) object MissingPrivateKey : Failure(null) - - data class PasswordAuthFlowMissing(val sessionId: String) : Failure(null) } interface BootstrapProgressListener { @@ -68,7 +62,7 @@ interface BootstrapProgressListener { } data class Params( - val userPasswordAuth: UserPasswordAuth? = null, + val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, val progressListener: BootstrapProgressListener? = null, val passphrase: String?, val keySpec: SsssKeySpec? = null, @@ -101,7 +95,10 @@ class BootstrapCrossSigningTask @Inject constructor( try { awaitCallback { - crossSigningService.initializeCrossSigning(params.userPasswordAuth, it) + crossSigningService.initializeCrossSigning( + params.userInteractiveAuthInterceptor, + it + ) } if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) { return BootstrapResult.SuccessCrossSigningOnly @@ -312,16 +309,6 @@ class BootstrapCrossSigningTask @Inject constructor( private fun handleInitializeXSigningError(failure: Throwable): BootstrapResult { if (failure is Failure.ServerError && failure.error.code == MatrixError.M_FORBIDDEN) { return BootstrapResult.InvalidPasswordError(failure.error) - } else { - val registrationFlowResponse = failure.toRegistrationFlowResponse() - if (registrationFlowResponse != null) { - return if (registrationFlowResponse.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true }) { - BootstrapResult.PasswordAuthFlowMissing(registrationFlowResponse.session ?: "") - } else { - // can't do this from here - BootstrapResult.UnsupportedAuthFlow() - } - } } return BootstrapResult.GenericError(failure) } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthFragment.kt new file mode 100644 index 0000000000..507050c2e8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthFragment.kt @@ -0,0 +1,84 @@ +/* + * 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.features.crypto.recover + +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.parentFragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.resources.ColorProvider +import im.vector.app.databinding.FragmentBootstrapReauthBinding + +import javax.inject.Inject + +class BootstrapReAuthFragment @Inject constructor( + private val colorProvider: ColorProvider +) : VectorBaseFragment() { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentBootstrapReauthBinding { + return FragmentBootstrapReauthBinding.inflate(inflater, container, false) + } + + val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + views.bootstrapRetryButton.debouncedClicks { submit() } + views.bootstrapCancelButton.debouncedClicks { cancel() } + } + + private fun submit() = withState(sharedViewModel) { state -> + if (state.step !is BootstrapStep.AccountReAuth) { + return@withState + } + if (state.passphrase != null) { + sharedViewModel.handle(BootstrapActions.DoInitialize(state.passphrase)) + } else { + sharedViewModel.handle(BootstrapActions.DoInitializeGeneratedKey) + } + } + + private fun cancel() = withState(sharedViewModel) { state -> + if (state.step !is BootstrapStep.AccountReAuth) { + return@withState + } + sharedViewModel.handle(BootstrapActions.GoBack) + } + + override fun invalidate() = withState(sharedViewModel) { state -> + if (state.step !is BootstrapStep.AccountReAuth) { + return@withState + } + val failure = state.step.failure + views.reAuthFailureText.setTextOrHide(failure) + if (failure == null) { + views.waitingProgress.isVisible = true + views.bootstrapCancelButton.isVisible = false + views.bootstrapRetryButton.isVisible = false + } else { + views.waitingProgress.isVisible = false + views.bootstrapCancelButton.isVisible = true + views.bootstrapRetryButton.isVisible = true + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt index 2e5097fdb7..42278cd948 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt @@ -25,25 +25,36 @@ import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import com.nulabinc.zxcvbn.Zxcvbn -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.R import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.resources.StringProvider +import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.login.ReAuthHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage +import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey -import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth +import org.matrix.android.sdk.api.auth.UIABaseAuth +import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.internal.util.awaitCallback import java.io.OutputStream +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume class BootstrapSharedViewModel @AssistedInject constructor( @Assisted initialState: BootstrapViewState, @@ -60,12 +71,15 @@ class BootstrapSharedViewModel @AssistedInject constructor( private var isBackupCreatedFromPassphrase: Boolean = false private val zxcvbn = Zxcvbn() - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: BootstrapViewState, args: BootstrapBottomSheet.Args): BootstrapSharedViewModel } - private var _pendingSession: String? = null +// private var _pendingSession: String? = null + + var uiaContinuation: Continuation? = null + var pendingAuth: UIABaseAuth? = null init { @@ -80,7 +94,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( SetupMode.CROSS_SIGNING_ONLY -> { // Go straight to account password setState { - copy(step = BootstrapStep.AccountPassword(false)) + copy(step = BootstrapStep.AccountReAuth()) } } SetupMode.NORMAL -> { @@ -148,10 +162,8 @@ class BootstrapSharedViewModel @AssistedInject constructor( copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible)) } } - is BootstrapStep.AccountPassword -> { - setState { - copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible)) - } + is BootstrapStep.AccountReAuth -> { + // nop } is BootstrapStep.GetBackupSecretPassForMigration -> { setState { @@ -195,16 +207,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( } is BootstrapActions.DoInitialize -> { if (state.passphrase == state.passphraseRepeat) { - val userPassword = reAuthHelper.data - if (userPassword == null) { - setState { - copy( - step = BootstrapStep.AccountPassword(false) - ) - } - } else { - startInitializeFlow(userPassword) - } + startInitializeFlow(state) } else { setState { copy( @@ -214,24 +217,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( } } is BootstrapActions.DoInitializeGeneratedKey -> { - val userPassword = reAuthHelper.data - if (userPassword == null) { - setState { - copy( - passphrase = null, - passphraseRepeat = null, - step = BootstrapStep.AccountPassword(false) - ) - } - } else { - setState { - copy( - passphrase = null, - passphraseRepeat = null - ) - } - startInitializeFlow(userPassword) - } + startInitializeFlow(state) } BootstrapActions.RecoveryKeySaved -> { _viewEvents.post(BootstrapViewEvents.RecoveryKeySaved) @@ -262,7 +248,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( } BootstrapActions.GoToEnterAccountPassword -> { setState { - copy(step = BootstrapStep.AccountPassword(false)) + copy(step = BootstrapStep.AccountReAuth()) } } BootstrapActions.HandleForgotBackupPassphrase -> { @@ -272,15 +258,33 @@ class BootstrapSharedViewModel @AssistedInject constructor( } } else return@withState } - is BootstrapActions.ReAuth -> { - startInitializeFlow(action.pass) - } +// is BootstrapActions.ReAuth -> { +// startInitializeFlow(action.pass) +// } is BootstrapActions.DoMigrateWithPassphrase -> { startMigrationFlow(state.step, action.passphrase, null) } is BootstrapActions.DoMigrateWithRecoveryKey -> { startMigrationFlow(state.step, null, action.recoveryKey) } + BootstrapActions.SsoAuthDone -> { + uiaContinuation?.resume(DefaultBaseAuth(session = pendingAuth?.session ?: "")) + } + is BootstrapActions.PasswordAuthDone -> { + val decryptedPass = session.loadSecureSecret(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS) + uiaContinuation?.resume( + UserPasswordAuth( + session = pendingAuth?.session, + password = decryptedPass, + user = session.myUserId + ) + ) + } + BootstrapActions.ReAuthCancelled -> { + setState { + copy(step = BootstrapStep.AccountReAuth(stringProvider.getString(R.string.authentication_error))) + } + } }.exhaustive } @@ -292,7 +296,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( ) } } else { - startInitializeFlow(null) + startInitializeFlow(it) } } @@ -345,16 +349,16 @@ class BootstrapSharedViewModel @AssistedInject constructor( migrationRecoveryKey = recoveryKey ) } - val userPassword = reAuthHelper.data - if (userPassword == null) { - setState { - copy( - step = BootstrapStep.AccountPassword(false) - ) - } - } else { - startInitializeFlow(userPassword) - } +// val userPassword = reAuthHelper.data +// if (userPassword == null) { +// setState { +// copy( +// step = BootstrapStep.AccountPassword(false) +// ) +// } +// } else { + withState { startInitializeFlow(it) } +// } } is BackupToQuadSMigrationTask.Result.Failure -> { _viewEvents.post( @@ -371,7 +375,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( } } - private fun startInitializeFlow(userPassword: String?) = withState { state -> + private fun startInitializeFlow(state: BootstrapViewState) { val previousStep = state.step setState { @@ -388,19 +392,45 @@ class BootstrapSharedViewModel @AssistedInject constructor( } } - viewModelScope.launch(Dispatchers.IO) { - val userPasswordAuth = userPassword?.let { - UserPasswordAuth( - // Note that _pendingSession may or may not be null, this is OK, it will be managed by the task - session = _pendingSession, - user = session.myUserId, - password = it - ) + val interceptor = object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + when (flowResponse.nextUncompletedStage()) { + LoginFlowTypes.PASSWORD -> { + pendingAuth = UserPasswordAuth( + // Note that _pendingSession may or may not be null, this is OK, it will be managed by the task + session = flowResponse.session, + user = session.myUserId, + password = null + ) + uiaContinuation = promise + setState { + copy( + step = BootstrapStep.AccountReAuth() + ) + } + _viewEvents.post(BootstrapViewEvents.RequestReAuth(flowResponse, errCode)) + } + LoginFlowTypes.SSO -> { + pendingAuth = DefaultBaseAuth(flowResponse.session) + uiaContinuation = promise + setState { + copy( + step = BootstrapStep.AccountReAuth() + ) + } + _viewEvents.post(BootstrapViewEvents.RequestReAuth(flowResponse, errCode)) + } + else -> { + promise.resumeWith(Result.failure(UnsupportedOperationException())) + } + } } + } + viewModelScope.launch(Dispatchers.IO) { bootstrapTask.invoke(this, Params( - userPasswordAuth = userPasswordAuth, + userInteractiveAuthInterceptor = interceptor, progressListener = progressListener, passphrase = state.passphrase, keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } }, @@ -409,7 +439,6 @@ class BootstrapSharedViewModel @AssistedInject constructor( ) { bootstrapResult -> when (bootstrapResult) { is BootstrapResult.SuccessCrossSigningOnly -> { - // TPD _viewEvents.post(BootstrapViewEvents.Dismiss(true)) } is BootstrapResult.Success -> { @@ -423,26 +452,11 @@ class BootstrapSharedViewModel @AssistedInject constructor( ) } } - is BootstrapResult.PasswordAuthFlowMissing -> { - // Ask the password to the user - _pendingSession = bootstrapResult.sessionId - setState { - copy( - step = BootstrapStep.AccountPassword(false) - ) - } - } - is BootstrapResult.UnsupportedAuthFlow -> { - _viewEvents.post(BootstrapViewEvents.ModalError(stringProvider.getString(R.string.auth_flow_not_supported))) - _viewEvents.post(BootstrapViewEvents.Dismiss(false)) - } is BootstrapResult.InvalidPasswordError -> { - // it's a bad password - // We clear the auth session, to avoid 'Requested operation has changed during the UI authentication session' error - _pendingSession = null + // it's a bad password / auth setState { copy( - step = BootstrapStep.AccountPassword(false, stringProvider.getString(R.string.auth_invalid_login_param)) + step = BootstrapStep.AccountReAuth(stringProvider.getString(R.string.auth_invalid_login_param)) ) } } @@ -515,7 +529,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( ) } } - is BootstrapStep.AccountPassword -> { + is BootstrapStep.AccountReAuth -> { _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null)) } BootstrapStep.Initializing -> { diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt index 222a5d78c6..09f0e90d5d 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt @@ -52,11 +52,11 @@ package im.vector.app.features.crypto.recover * │ │ BootstrapStep.ConfirmPassphrase │──┐ * │ └────────────────────────────────────┘ │ * │ │ │ - * │ is password needed? │ + * │ is password/reauth needed? │ * │ │ │ * │ ▼ │ * │ ┌────────────────────────────────────┐ │ - * │ │ BootstrapStep.AccountPassword │ │ + * │ │ BootstrapStep.AccountReAuth │ │ * │ └────────────────────────────────────┘ │ * │ │ │ * │ │ │ @@ -94,7 +94,7 @@ sealed class BootstrapStep { data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep() data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep() - data class AccountPassword(val isPasswordVisible: Boolean, val failure: String? = null) : BootstrapStep() + data class AccountReAuth(val failure: String? = null) : BootstrapStep() abstract class GetBackupSecretForMigration : BootstrapStep() data class GetBackupSecretPassForMigration(val isPasswordVisible: Boolean, val useKey: Boolean) : GetBackupSecretForMigration() diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewEvents.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewEvents.kt index 10a092ccbb..3f06623ad6 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewEvents.kt @@ -17,10 +17,12 @@ package im.vector.app.features.crypto.recover import im.vector.app.core.platform.VectorViewEvents +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse sealed class BootstrapViewEvents : VectorViewEvents { data class Dismiss(val success: Boolean) : BootstrapViewEvents() data class ModalError(val error: String) : BootstrapViewEvents() object RecoveryKeySaved : BootstrapViewEvents() data class SkipBootstrap(val genKeyOption: Boolean = true) : BootstrapViewEvents() + data class RequestReAuth(val flowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : BootstrapViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt index 23ed9b6483..04ac79d4a4 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -25,8 +25,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel @@ -159,7 +160,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( super.onCleared() } - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: VerificationBottomSheetViewState, args: VerificationBottomSheet.VerificationArgs): VerificationBottomSheetViewModel diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt index 4518a8d6b7..7b9acd2f57 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt @@ -19,8 +19,9 @@ import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.di.HasScreenInjector import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents @@ -79,7 +80,7 @@ class VerificationChooseMethodViewModel @AssistedInject constructor( } } - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: VerificationChooseMethodViewState): VerificationChooseMethodViewModel } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt index d511123f5e..44f0e752c8 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt @@ -24,8 +24,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.di.HasScreenInjector import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents @@ -149,7 +150,7 @@ class VerificationEmojiCodeViewModel @AssistedInject constructor( } } - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: VerificationEmojiCodeViewState): VerificationEmojiCodeViewModel } diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt index d16ebcdc8b..bf2defafa1 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt @@ -24,8 +24,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.launch @@ -42,7 +43,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( private val session: Session) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: DiscoverySettingsState): DiscoverySettingsViewModel } diff --git a/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerViewModel.kt b/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerViewModel.kt index 0f07a0353f..9455b1bff4 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/change/SetIdentityServerViewModel.kt @@ -19,8 +19,9 @@ import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.R import im.vector.app.core.di.HasScreenInjector import im.vector.app.core.extensions.exhaustive @@ -41,7 +42,7 @@ class SetIdentityServerViewModel @AssistedInject constructor( stringProvider: StringProvider) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: SetIdentityServerState): SetIdentityServerViewModel } diff --git a/vector/src/main/java/im/vector/app/features/grouplist/GroupListViewModel.kt b/vector/src/main/java/im/vector/app/features/grouplist/GroupListViewModel.kt index a17aa4dbf2..3b096adbfb 100644 --- a/vector/src/main/java/im/vector/app/features/grouplist/GroupListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/grouplist/GroupListViewModel.kt @@ -22,8 +22,9 @@ import arrow.core.Option import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.R import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider @@ -45,7 +46,7 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro private val stringProvider: StringProvider ) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: GroupListViewState): GroupListViewModel } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 90d128320b..afaa290190 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -20,29 +20,38 @@ import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.login.ReAuthHelper import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch -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.auth.data.LoginFlowTypes +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.pushrules.RuleIds import org.matrix.android.sdk.api.session.InitialSyncProgressService import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.util.toMatrixItem +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage 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.matrix.android.sdk.api.auth.UIABaseAuth +import org.matrix.android.sdk.api.auth.UserPasswordAuth +import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.asObservable import org.matrix.android.sdk.rx.rx import timber.log.Timber +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume class HomeActivityViewModel @AssistedInject constructor( @Assisted initialState: HomeActivityViewState, @@ -52,7 +61,7 @@ class HomeActivityViewModel @AssistedInject constructor( private val vectorPreferences: VectorPreferences ) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: HomeActivityViewState, args: HomeActivityArgs): HomeActivityViewModel } @@ -73,7 +82,6 @@ class HomeActivityViewModel @AssistedInject constructor( init { cleanupFiles() observeInitialSync() - mayBeInitializeCrossSigning() checkSessionPushIsOn() observeCrossSigningReset() } @@ -121,10 +129,10 @@ class HomeActivityViewModel @AssistedInject constructor( // Schedule a check of the bootstrap when the init sync will be finished checkBootstrap = true } - is InitialSyncProgressService.Status.Idle -> { + is InitialSyncProgressService.Status.Idle -> { if (checkBootstrap) { checkBootstrap = false - maybeBootstrapCrossSigning() + maybeBootstrapCrossSigningAfterInitialSync() } } } @@ -138,29 +146,6 @@ class HomeActivityViewModel @AssistedInject constructor( .disposeOnClear() } - private fun mayBeInitializeCrossSigning() { - if (args.accountCreation) { - val password = reAuthHelper.data ?: return Unit.also { - Timber.w("No password to init cross signing") - } - - val session = activeSessionHolder.getSafeActiveSession() ?: return Unit.also { - Timber.w("No session to init cross signing") - } - - // We do not use the viewModel context because we do not want to cancel this action - Timber.d("Initialize cross signing") - session.cryptoService().crossSigningService().initializeCrossSigning( - authParams = UserPasswordAuth( - session = null, - user = session.myUserId, - password = password - ), - callback = NoOpMatrixCallback() - ) - } - } - /** * After migration from riot to element some users reported that their * push setting for the session was set to off @@ -196,56 +181,66 @@ class HomeActivityViewModel @AssistedInject constructor( } } - private fun maybeBootstrapCrossSigning() { - // In case of account creation, it is already done before - if (args.accountCreation) return + private fun maybeBootstrapCrossSigningAfterInitialSync() { + // We do not use the viewModel context because we do not want to tie this action to activity view model + GlobalScope.launch(Dispatchers.IO) { + val session = activeSessionHolder.getSafeActiveSession() ?: return@launch - val session = activeSessionHolder.getSafeActiveSession() ?: return + tryOrNull("## MaybeBootstrapCrossSigning: Failed to download keys") { + awaitCallback> { + session.cryptoService().downloadKeys(listOf(session.myUserId), true, it) + } + } - // Ensure keys of the user are downloaded - session.cryptoService().downloadKeys(listOf(session.myUserId), true, object : MatrixCallback> { - override fun onSuccess(data: MXUsersDevicesMap) { - // Is there already cross signing keys here? - val mxCrossSigningInfo = session.cryptoService().crossSigningService().getMyCrossSigningKeys() - if (mxCrossSigningInfo != null) { - // Cross-signing is already set up for this user, is it trusted? - if (!mxCrossSigningInfo.isTrusted()) { - // New session - _viewEvents.post( - HomeActivityViewEvents.OnNewSession( - session.getUser(session.myUserId)?.toMatrixItem(), - // If it's an old unverified, we should send requests - // instead of waiting for an incoming one - reAuthHelper.data != null - ) - ) - } - } else { - // Initialize cross-signing - val password = reAuthHelper.data - - if (password == null) { - // Check this is not an SSO account - if (session.getHomeServerCapabilities().canChangePassword) { - // Ask password to the user: Upgrade security - _viewEvents.post(HomeActivityViewEvents.AskPasswordToInitCrossSigning(session.getUser(session.myUserId)?.toMatrixItem())) - } - // Else (SSO) just ignore for the moment - } else { - // We do not use the viewModel context because we do not want to cancel this action - Timber.d("Initialize cross signing") + // From there we are up to date with server + // Is there already cross signing keys here? + val mxCrossSigningInfo = session.cryptoService().crossSigningService().getMyCrossSigningKeys() + if (mxCrossSigningInfo != null) { + // Cross-signing is already set up for this user, is it trusted? + if (!mxCrossSigningInfo.isTrusted()) { + // New session + _viewEvents.post( + HomeActivityViewEvents.OnNewSession( + session.getUser(session.myUserId)?.toMatrixItem(), + // If it's an old unverified, we should send requests + // instead of waiting for an incoming one + reAuthHelper.data != null + ) + ) + } + } else { + // Try to initialize cross signing in background if possible + Timber.d("Initialize cross signing...") + awaitCallback { + try { session.cryptoService().crossSigningService().initializeCrossSigning( - authParams = UserPasswordAuth( - session = null, - user = session.myUserId, - password = password - ), - callback = NoOpMatrixCallback() + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + // We missed server grace period or it's not setup, see if we remember locally password + if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD + && errCode == null + && reAuthHelper.data != null) { + promise.resume( + UserPasswordAuth( + session = flowResponse.session, + user = session.myUserId, + password = reAuthHelper.data + ) + ) + } else { + promise.resumeWith(Result.failure(Exception("Cannot silently initialize cross signing, UIA missing"))) + } + } + }, + callback = it ) + Timber.d("Initialize cross signing SUCCESS") + } catch (failure: Throwable) { + Timber.e(failure, "Failed to initialize cross signing") } } } - }) + } } override fun handle(action: HomeActivityViewActions) { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt index 88c310fde8..c261081055 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt @@ -19,8 +19,9 @@ package im.vector.app.features.home import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.di.HasScreenInjector import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -44,7 +45,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho private val stringProvider: StringProvider) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: HomeDetailViewState): HomeDetailViewModel } diff --git a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt index 3bdcfc4018..f4f16502ab 100644 --- a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt @@ -24,8 +24,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModelAction @@ -65,7 +66,7 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(@Assisted data class IgnoreDevice(val deviceIds: List) : Action() } - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel } diff --git a/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsViewModel.kt index 8a32157097..d3825de4ef 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsViewModel.kt @@ -19,8 +19,9 @@ package im.vector.app.features.home.room.breadcrumbs import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -35,7 +36,7 @@ class BreadcrumbsViewModel @AssistedInject constructor(@Assisted initialState: B private val session: Session) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: BreadcrumbsViewState): BreadcrumbsViewModel } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt index d560a675fe..ec2ff82e67 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt @@ -24,8 +24,9 @@ import android.widget.EditText import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.CharPolicy -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.R import im.vector.app.core.glide.GlideApp import im.vector.app.core.glide.GlideRequests @@ -59,7 +60,7 @@ class AutoCompleter @AssistedInject constructor( private lateinit var autocompleteMemberPresenter: AutocompleteMemberPresenter - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(roomId: String): AutoCompleter } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index e134230c61..2d2059377c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -297,6 +297,8 @@ class RoomDetailFragment @Inject constructor( private var lockSendButton = false private val activeCallViewHolder = ActiveCallViewHolder() + private lateinit var emojiPopup: EmojiPopup + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) @@ -512,7 +514,7 @@ class RoomDetailFragment @Inject constructor( } private fun setupEmojiPopup() { - val emojiPopup = EmojiPopup + emojiPopup = EmojiPopup .Builder .fromRootView(views.rootConstraintLayout) .setKeyboardAnimationStyle(R.style.emoji_fade_animation_style) @@ -591,6 +593,7 @@ class RoomDetailFragment @Inject constructor( autoCompleter.clear() debouncer.cancelAll() views.timelineRecyclerView.cleanup() + emojiPopup.dismiss() super.onDestroyView() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 975eda5d2f..09179a9458 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -25,8 +25,9 @@ import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext import com.jakewharton.rxrelay2.BehaviorRelay import com.jakewharton.rxrelay2.PublishRelay -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel @@ -134,7 +135,7 @@ class RoomDetailViewModel @AssistedInject constructor( private var trackUnreadMessages = AtomicBoolean(false) private var mostRecentDisplayedEvent: TimelineEvent? = null - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: RoomDetailViewState): RoomDetailViewModel } @@ -1395,7 +1396,7 @@ class RoomDetailViewModel @AssistedInject constructor( snapshot .takeIf { state.asyncRoomSummary.invoke()?.isEncrypted == false } ?.forEach { - previewUrlRetriever.getPreviewUrl(it.root, viewModelScope) + previewUrlRetriever.getPreviewUrl(it, viewModelScope) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt index 8a8701e45f..9a57d9480c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt @@ -76,10 +76,10 @@ class SearchFragment @Inject constructor( controller.listener = this } - override fun onDestroy() { - super.onDestroy() + override fun onDestroyView() { views.searchResultRecycler.cleanup() controller.listener = null + super.onDestroyView() } override fun invalidate() = withState(searchViewModel) { state -> diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt index ab440f6b5f..cb93cf95d2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt @@ -23,8 +23,9 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.launch @@ -46,7 +47,7 @@ class SearchViewModel @AssistedInject constructor( private var nextBatch: String? = null - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: SearchViewState): SearchViewModel } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 2eae58c6b9..5008f0e0aa 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -18,8 +18,9 @@ package im.vector.app.features.home.room.detail.timeline.action import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import dagger.Lazy import im.vector.app.R import im.vector.app.core.extensions.canReact @@ -75,7 +76,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted pillsPostProcessorFactory.create(initialState.roomId) } - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: MessageActionState): MessageActionsViewModel } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt index e4d124dd94..fff1c8a0ff 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/edithistory/ViewEditHistoryViewModel.kt @@ -24,8 +24,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents @@ -61,7 +62,7 @@ class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted private val room = session.getRoom(roomId) ?: throw IllegalStateException("Shouldn't use this ViewModel without a room") - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: ViewEditHistoryViewState): ViewEditHistoryViewModel } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt index a36f656f52..c1f98cc3ab 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt @@ -22,8 +22,9 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.platform.EmptyAction @@ -68,7 +69,7 @@ class ViewReactionsViewModel @AssistedInject constructor(@Assisted private val room = session.getRoom(roomId) ?: throw IllegalStateException("Shouldn't use this ViewModel without a room") - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: DisplayReactionsViewState): ViewReactionsViewModel } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt index 695661feeb..54d5fd9eb3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt @@ -16,37 +16,56 @@ package im.vector.app.features.home.room.detail.timeline.url +import android.os.Handler +import android.os.Looper import im.vector.app.BuildConfig import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.matrix.android.sdk.api.cache.CacheStrategy import org.matrix.android.sdk.api.session.Session -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.session.room.timeline.getLatestEventId class PreviewUrlRetriever(session: Session) { private val mediaService = session.mediaService() - private val data = mutableMapOf() + private data class EventIdPreviewUrlUiState( + // Id of the latest event in the case of an edited event, or the eventId for an event which has not been edited + val latestEventId: String, + val previewUrlUiState: PreviewUrlUiState + ) + + // Keys are the main eventId + private val data = mutableMapOf() private val listeners = mutableMapOf>() + private val uiHandler = Handler(Looper.getMainLooper()) // In memory list private val blockedUrl = mutableSetOf() - fun getPreviewUrl(event: Event, coroutineScope: CoroutineScope) { - val eventId = event.eventId ?: return + fun getPreviewUrl(event: TimelineEvent, coroutineScope: CoroutineScope) { + val eventId = event.root.eventId ?: return + val latestEventId = event.getLatestEventId() synchronized(data) { - if (data[eventId] == null) { + val current = data[eventId] + if (current?.latestEventId != latestEventId) { + // The event is not known or it has been edited // Keep only the first URL for the moment val url = mediaService.extractUrls(event) .firstOrNull() ?.takeIf { it !in blockedUrl } if (url == null) { - updateState(eventId, PreviewUrlUiState.NoUrl) + updateState(eventId, latestEventId, PreviewUrlUiState.NoUrl) + null + } else if (url != (current?.previewUrlUiState as? PreviewUrlUiState.Data)?.url) { + // There is a not known URL, or the Event has been edited and the URL has changed + updateState(eventId, latestEventId, PreviewUrlUiState.Loading) + url } else { - updateState(eventId, PreviewUrlUiState.Loading) + // Already handled + null } - url } else { // Already handled null @@ -64,15 +83,15 @@ class PreviewUrlRetriever(session: Session) { synchronized(data) { // Blocked after the request has been sent? if (urlToRetrieve in blockedUrl) { - updateState(eventId, PreviewUrlUiState.NoUrl) + updateState(eventId, latestEventId, PreviewUrlUiState.NoUrl) } else { - updateState(eventId, PreviewUrlUiState.Data(eventId, urlToRetrieve, it)) + updateState(eventId, latestEventId, PreviewUrlUiState.Data(eventId, urlToRetrieve, it)) } } }, { synchronized(data) { - updateState(eventId, PreviewUrlUiState.Error(it)) + updateState(eventId, latestEventId, PreviewUrlUiState.Error(it)) } } ) @@ -86,18 +105,20 @@ class PreviewUrlRetriever(session: Session) { // Notify the listener synchronized(data) { data[eventId] - ?.takeIf { it is PreviewUrlUiState.Data && it.url == url } + ?.takeIf { it.previewUrlUiState is PreviewUrlUiState.Data && it.previewUrlUiState.url == url } ?.let { - updateState(eventId, PreviewUrlUiState.NoUrl) + updateState(eventId, it.latestEventId, PreviewUrlUiState.NoUrl) } } } - private fun updateState(eventId: String, state: PreviewUrlUiState) { - data[eventId] = state + private fun updateState(eventId: String, latestEventId: String, state: PreviewUrlUiState) { + data[eventId] = EventIdPreviewUrlUiState(latestEventId, state) // Notify the listener - listeners[eventId].orEmpty().forEach { - it.onStateUpdated(state) + uiHandler.post { + listeners[eventId].orEmpty().forEach { + it.onStateUpdated(state) + } } } @@ -107,7 +128,7 @@ class PreviewUrlRetriever(session: Session) { // Give the current state if any synchronized(data) { - listener.onStateUpdated(data[key] ?: PreviewUrlUiState.Unknown) + listener.onStateUpdated(data[key]?.previewUrlUiState ?: PreviewUrlUiState.Unknown) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsViewModel.kt index a062b2b034..c51c571815 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsViewModel.kt @@ -18,8 +18,9 @@ package im.vector.app.features.home.room.list.actions import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -31,7 +32,7 @@ class RoomListQuickActionsViewModel @AssistedInject constructor(@Assisted initia private val session: Session ) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: RoomListQuickActionsState): RoomListQuickActionsViewModel } diff --git a/vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewModel.kt b/vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewModel.kt index fe7a8006e0..05fd825558 100644 --- a/vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewModel.kt @@ -20,8 +20,9 @@ import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.di.HasScreenInjector import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents @@ -42,7 +43,7 @@ class HomeServerCapabilitiesViewModel @AssistedInject constructor( private val rawService: RawService ) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: HomeServerCapabilitiesViewState): HomeServerCapabilitiesViewModel } diff --git a/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt b/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt index c13f5fdfb3..716baff757 100644 --- a/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt +++ b/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt @@ -19,8 +19,9 @@ package im.vector.app.features.html import android.content.Context import android.text.Spannable import android.text.Spanned -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.glide.GlideApp import im.vector.app.features.home.AvatarRenderer @@ -37,7 +38,7 @@ class PillsPostProcessor @AssistedInject constructor(@Assisted private val roomI private val sessionHolder: ActiveSessionHolder) : EventHtmlRenderer.PostProcessor { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(roomId: String?): PillsPostProcessor } diff --git a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt index 21a998d8e2..a694ee36ba 100644 --- a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt @@ -19,8 +19,9 @@ package im.vector.app.features.invite import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.R import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider @@ -37,7 +38,7 @@ class InviteUsersToRoomViewModel @AssistedInject constructor(@Assisted private val room = session.getRoom(initialState.roomId)!! - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: InviteUsersToRoomViewState): InviteUsersToRoomViewModel } diff --git a/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt b/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt index 2f1e8c0b1a..6c0e142b38 100644 --- a/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt @@ -19,6 +19,7 @@ package im.vector.app.features.link import android.content.Intent import android.net.Uri import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.lifecycleScope import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ScreenComponent @@ -30,7 +31,7 @@ import im.vector.app.features.login.LoginActivity import im.vector.app.features.login.LoginConfig import im.vector.app.features.permalink.PermalinkHandler import io.reactivex.android.schedulers.AndroidSchedulers -import org.matrix.android.sdk.api.MatrixCallback +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.permalinks.PermalinkService import timber.log.Timber import java.util.concurrent.TimeUnit @@ -139,23 +140,30 @@ class LinkHandlerActivity : VectorBaseActivity() { .setTitle(R.string.dialog_title_warning) .setMessage(R.string.error_user_already_logged_in) .setCancelable(false) - .setPositiveButton(R.string.logout) { _, _ -> - sessionHolder.getSafeActiveSession()?.signOut(true, object : MatrixCallback { - override fun onFailure(failure: Throwable) { - displayError(failure) - } - - override fun onSuccess(data: Unit) { - Timber.d("## displayAlreadyLoginPopup(): logout succeeded") - sessionHolder.clearActiveSession() - startLoginActivity(uri) - } - }) ?: finish() - } + .setPositiveButton(R.string.logout) { _, _ -> safeSignout(uri) } .setNegativeButton(R.string.cancel) { _, _ -> finish() } .show() } + private fun safeSignout(uri: Uri) { + val session = sessionHolder.getSafeActiveSession() + if (session == null) { + // Should not happen + startLoginActivity(uri) + } else { + lifecycleScope.launch { + try { + session.signOut(true) + Timber.d("## displayAlreadyLoginPopup(): logout succeeded") + sessionHolder.clearActiveSession() + startLoginActivity(uri) + } catch (failure: Throwable) { + displayError(failure) + } + } + } + } + private fun displayError(failure: Throwable) { AlertDialog.Builder(this) .setTitle(R.string.dialog_title_error) diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt index a3f1c36d08..7bf0b98841 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt @@ -26,8 +26,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.configureAndStart @@ -68,7 +69,7 @@ class LoginViewModel @AssistedInject constructor( private val homeServerHistoryService: HomeServerHistoryService ) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: LoginViewState): LoginViewModel } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt index 4b03c93321..9ea42308eb 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt @@ -155,18 +155,15 @@ class LoginWebFragment @Inject constructor( // avoid infinite onPageFinished call if (url.startsWith("http")) { // Generic method to make a bridge between JS and the UIWebView - val mxcJavascriptSendObjectMessage = assetReader.readAssetFile("sendObject.js") - view.loadUrl(mxcJavascriptSendObjectMessage) + assetReader.readAssetFile("sendObject.js")?.let { view.loadUrl(it) } if (state.signMode == SignMode.SignIn) { // The function the fallback page calls when the login is complete - val mxcJavascriptOnLogin = assetReader.readAssetFile("onLogin.js") - view.loadUrl(mxcJavascriptOnLogin) + assetReader.readAssetFile("onLogin.js")?.let { view.loadUrl(it) } } else { // MODE_REGISTER // The function the fallback page calls when the registration is complete - val mxcJavascriptOnRegistered = assetReader.readAssetFile("onRegistered.js") - view.loadUrl(mxcJavascriptOnRegistered) + assetReader.readAssetFile("onRegistered.js")?.let { view.loadUrl(it) } } } } diff --git a/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt index 9290479a7a..4dc688ad22 100644 --- a/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt +++ b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt @@ -83,25 +83,28 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: ssoIdentityProviders?.forEach { identityProvider -> // Use some heuristic to render buttons according to branding guidelines val button: MaterialButton = cachedViews[identityProvider.id] - ?: when (identityProvider.id) { - SsoIdentityProvider.ID_GOOGLE -> { + ?: when (identityProvider.brand) { + SsoIdentityProvider.BRAND_GOOGLE -> { MaterialButton(context, null, R.attr.vctr_social_login_button_google_style) } - SsoIdentityProvider.ID_GITHUB -> { + SsoIdentityProvider.BRAND_GITHUB -> { MaterialButton(context, null, R.attr.vctr_social_login_button_github_style) } - SsoIdentityProvider.ID_APPLE -> { + SsoIdentityProvider.BRAND_APPLE -> { MaterialButton(context, null, R.attr.vctr_social_login_button_apple_style) } - SsoIdentityProvider.ID_FACEBOOK -> { + SsoIdentityProvider.BRAND_FACEBOOK -> { MaterialButton(context, null, R.attr.vctr_social_login_button_facebook_style) } - SsoIdentityProvider.ID_TWITTER -> { + SsoIdentityProvider.BRAND_TWITTER -> { MaterialButton(context, null, R.attr.vctr_social_login_button_twitter_style) } + SsoIdentityProvider.BRAND_GITLAB -> { + MaterialButton(context, null, R.attr.vctr_social_login_button_gitlab_style) + } else -> { // TODO Use iconUrl - MaterialButton(context, null, R.attr.materialButtonStyle).apply { + MaterialButton(context, null, R.attr.materialButtonOutlinedStyle).apply { transformationMethod = null textAlignment = View.TEXT_ALIGNMENT_CENTER } @@ -131,12 +134,13 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: clipChildren = false if (isInEditMode) { ssoIdentityProviders = listOf( - SsoIdentityProvider(SsoIdentityProvider.ID_GOOGLE, "Google", null), - SsoIdentityProvider(SsoIdentityProvider.ID_FACEBOOK, "Facebook", null), - SsoIdentityProvider(SsoIdentityProvider.ID_APPLE, "Apple", null), - SsoIdentityProvider(SsoIdentityProvider.ID_GITHUB, "GitHub", null), - SsoIdentityProvider(SsoIdentityProvider.ID_TWITTER, "Twitter", null), - SsoIdentityProvider("Custom_pro", "SSO", null) + SsoIdentityProvider("Google", "Google", null, SsoIdentityProvider.BRAND_GOOGLE), + SsoIdentityProvider("Facebook", "Facebook", null, SsoIdentityProvider.BRAND_FACEBOOK), + SsoIdentityProvider("Apple", "Apple", null, SsoIdentityProvider.BRAND_APPLE), + SsoIdentityProvider("GitHub", "GitHub", null, SsoIdentityProvider.BRAND_GITHUB), + SsoIdentityProvider("Twitter", "Twitter", null, SsoIdentityProvider.BRAND_TWITTER), + SsoIdentityProvider("Gitlab", "Gitlab", null, SsoIdentityProvider.BRAND_GITLAB), + SsoIdentityProvider("Custom_pro", "SSO", null, null) ) } val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.SocialLoginButtonsView, 0, 0) diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt index 6e8a530c9a..207256c75a 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt @@ -24,8 +24,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel @@ -50,7 +51,7 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor( private val stringProvider: StringProvider, private val rawService: RawService) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: MatrixToBottomSheetState): MatrixToBottomSheetViewModel } diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 0a6197e424..fded8602c4 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -229,10 +229,13 @@ class DefaultNavigator @Inject constructor( } override fun openKeysBackupSetup(context: Context, showManualExport: Boolean) { - // if cross signing is enabled we should propose full 4S + // if cross signing is enabled and trusted or not set up at all we should propose full 4S sessionHolder.getSafeActiveSession()?.let { session -> - if (session.cryptoService().crossSigningService().canCrossSign() && context is AppCompatActivity) { - BootstrapBottomSheet.show(context.supportFragmentManager, SetupMode.NORMAL) + if (session.cryptoService().crossSigningService().getMyCrossSigningKeys() == null + || session.cryptoService().crossSigningService().canCrossSign()) { + (context as? AppCompatActivity)?.let { + BootstrapBottomSheet.show(it.supportFragmentManager, SetupMode.NORMAL) + } } else { context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport)) } diff --git a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt index 24fb6159f8..28b2a8b4d5 100644 --- a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt +++ b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt @@ -21,6 +21,7 @@ import android.os.Build import android.os.Handler import android.os.Looper import android.view.View +import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS import android.widget.ImageView import com.tapadoo.alerter.Alerter import com.tapadoo.alerter.OnHideAlertListener @@ -165,9 +166,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy - var flags = view.systemUiVisibility - flags = flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() - view.systemUiVisibility = flags + view.windowInsetsController?.setSystemBarsAppearance(0, APPEARANCE_LIGHT_STATUS_BARS) } } @@ -179,9 +178,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy - var flags = view.systemUiVisibility - flags = flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR - view.systemUiVisibility = flags + view.windowInsetsController?.setSystemBarsAppearance(APPEARANCE_LIGHT_STATUS_BARS, APPEARANCE_LIGHT_STATUS_BARS) } } diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt index 7be7624a48..1a03fc6c47 100755 --- a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt @@ -21,7 +21,6 @@ import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.graphics.Canvas -import android.os.AsyncTask import android.os.Build import android.view.View import androidx.fragment.app.DialogFragment @@ -37,6 +36,11 @@ import im.vector.app.features.settings.devtools.GossipingEventsSerializer import im.vector.app.features.settings.locale.SystemLocaleProvider import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.version.VersionProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import okhttp3.Call import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient @@ -98,6 +102,8 @@ class BugReporter @Inject constructor( var screenshot: Bitmap? = null private set + private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + private val LOGCAT_CMD_ERROR = arrayOf("logcat", // /< Run 'logcat' command "-d", // /< Dump the log rather than continue outputting it "-v", // formatting @@ -160,13 +166,13 @@ class BugReporter @Inject constructor( withScreenshot: Boolean, theBugDescription: String, listener: IMXBugReportListener?) { - object : AsyncTask() { - // enumerate files to delete - val mBugReportFiles: MutableList = ArrayList() + // enumerate files to delete + val mBugReportFiles: MutableList = ArrayList() - override fun doInBackground(vararg voids: Void?): String? { + coroutineScope.launch { + var serverError: String? = null + withContext(Dispatchers.IO) { var bugDescription = theBugDescription - var serverError: String? = null val crashCallStack = getCrashDescription(context) if (null != crashCallStack) { @@ -342,7 +348,11 @@ class BugReporter @Inject constructor( } Timber.v("## onWrite() : $percentage%") - publishProgress(percentage) + try { + listener?.onProgress(percentage) + } catch (e: Exception) { + Timber.e(e, "## onProgress() : failed") + } } // build the request @@ -386,11 +396,13 @@ class BugReporter @Inject constructor( } // check if the error message - try { - val responseJSON = JSONObject(serverError) - serverError = responseJSON.getString("error") - } catch (e: JSONException) { - Timber.e(e, "doInBackground ; Json conversion failed") + serverError?.let { + try { + val responseJSON = JSONObject(it) + serverError = responseJSON.getString("error") + } catch (e: JSONException) { + Timber.e(e, "doInBackground ; Json conversion failed") + } } // should never happen @@ -403,21 +415,9 @@ class BugReporter @Inject constructor( } } } - - return serverError } - override fun onProgressUpdate(vararg progress: Int?) { - if (null != listener) { - try { - listener.onProgress(progress[0] ?: 0) - } catch (e: Exception) { - Timber.e(e, "## onProgress() : failed") - } - } - } - - override fun onPostExecute(reason: String?) { + withContext(Dispatchers.Main) { mBugReportCall = null // delete when the bug report has been successfully sent @@ -429,17 +429,17 @@ class BugReporter @Inject constructor( try { if (mIsCancelled) { listener.onUploadCancelled() - } else if (null == reason) { + } else if (null == serverError) { listener.onUploadSucceed() } else { - listener.onUploadFailed(reason) + listener.onUploadFailed(serverError) } } catch (e: Exception) { Timber.e(e, "## onPostExecute() : failed") } } } - }.execute() + } } /** @@ -457,9 +457,9 @@ class BugReporter @Inject constructor( activity.startActivity(intent) } - // ============================================================================================================== - // crash report management - // ============================================================================================================== +// ============================================================================================================== +// crash report management +// ============================================================================================================== /** * Provides the crash file @@ -529,9 +529,9 @@ class BugReporter @Inject constructor( return null } - // ============================================================================================================== - // Screenshot management - // ============================================================================================================== +// ============================================================================================================== +// Screenshot management +// ============================================================================================================== /** * Take a screenshot of the display. @@ -598,9 +598,9 @@ class BugReporter @Inject constructor( } } - // ============================================================================================================== - // Logcat management - // ============================================================================================================== +// ============================================================================================================== +// Logcat management +// ============================================================================================================== /** * Save the logcat @@ -660,9 +660,9 @@ class BugReporter @Inject constructor( } } - // ============================================================================================================== - // File compression management - // ============================================================================================================== +// ============================================================================================================== +// File compression management +// ============================================================================================================== /** * GZip a file diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultViewModel.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultViewModel.kt index 98b729603a..ac7aee797a 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultViewModel.kt @@ -19,8 +19,9 @@ import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.reactions.data.EmojiDataSource @@ -36,7 +37,7 @@ class EmojiSearchResultViewModel @AssistedInject constructor( private val dataSource: EmojiDataSource) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: EmojiSearchResultViewState): EmojiSearchResultViewModel } diff --git a/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewModel.kt b/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewModel.kt index 868a4e4dd6..c8b0037311 100644 --- a/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/room/RequireActiveMembershipViewModel.kt @@ -21,8 +21,9 @@ import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import com.jakewharton.rxrelay2.BehaviorRelay -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel @@ -48,7 +49,7 @@ class RequireActiveMembershipViewModel @AssistedInject constructor( private val session: Session) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: RequireActiveMembershipViewState): RequireActiveMembershipViewModel } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt index b047b2ede9..f5c545c34d 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt @@ -23,8 +23,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.appendAt -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.platform.VectorViewModel import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.extensions.orFalse @@ -46,7 +47,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: private val session: Session) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: PublicRoomsViewState): RoomDirectoryViewModel } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt index 216a016fbe..3c027c4845 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt @@ -25,8 +25,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.raw.wellknown.getElementWellknown @@ -46,7 +47,7 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr private val rawService: RawService ) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: CreateRoomViewState): CreateRoomViewModel } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt index 44044304bc..5460411907 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt @@ -21,8 +21,9 @@ import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import org.matrix.android.sdk.api.MatrixCallback @@ -33,7 +34,7 @@ class RoomDirectoryPickerViewModel @AssistedInject constructor(@Assisted initial private val session: Session) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: RoomDirectoryPickerViewState): RoomDirectoryPickerViewModel } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt index 63b9a50546..c99213d890 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewViewModel.kt @@ -22,8 +22,9 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -46,7 +47,7 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val ini private val session: Session) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: RoomPreviewViewState): RoomPreviewViewModel } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt index e29c197ab8..a692eebe40 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt @@ -80,8 +80,6 @@ class RoomMemberProfileController @Inject constructor( action = { callback?.onIgnoreClicked() } ) if (!state.isMine) { - buildProfileSection(stringProvider.getString(R.string.room_profile_section_more)) - buildProfileAction( id = "direct", editable = false, diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt index 8211a05127..0556b9d2d6 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt @@ -25,8 +25,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.R import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider @@ -61,7 +62,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v private val session: Session) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: RoomMemberProfileViewState): RoomMemberProfileViewModel } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt index 0f1ac3bfea..7d31a5c811 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt @@ -22,8 +22,9 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.di.HasScreenInjector import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel @@ -48,7 +49,7 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva private val session: Session) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: DeviceListViewState, args: DeviceListBottomSheet.Args): DeviceListBottomSheetViewModel } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt index cfdf1d9c0b..02a648287f 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt @@ -21,8 +21,9 @@ import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel @@ -49,7 +50,7 @@ class RoomProfileViewModel @AssistedInject constructor( private val session: Session ) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: RoomProfileViewState): RoomProfileViewModel } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt index f470eeefc2..8832c9f7d8 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt @@ -24,8 +24,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.powerlevel.PowerLevelsObservableFactory @@ -44,7 +45,7 @@ class RoomAliasViewModel @AssistedInject constructor(@Assisted initialState: Roo private val session: Session) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: RoomAliasViewState): RoomAliasViewModel } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetViewModel.kt index 7f723cae53..e762b52025 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/detail/RoomAliasBottomSheetViewModel.kt @@ -18,8 +18,9 @@ package im.vector.app.features.roomprofile.alias.detail import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -30,7 +31,7 @@ class RoomAliasBottomSheetViewModel @AssistedInject constructor( session: Session ) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: RoomAliasBottomSheetState): RoomAliasBottomSheetViewModel } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt index 0cecd22fa0..5663392c6c 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt @@ -20,8 +20,9 @@ import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel @@ -47,7 +48,7 @@ class RoomBannedMemberListViewModel @AssistedInject constructor(@Assisted initia private val session: Session) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: RoomBannedMemberListViewState): RoomBannedMemberListViewModel } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt index 9f15e62b3b..de983537fc 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt @@ -20,8 +20,9 @@ import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -53,7 +54,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState private val session: Session) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: RoomMemberListViewState): RoomMemberListViewModel } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewModel.kt index 183488a5e0..626c20010c 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewModel.kt @@ -21,8 +21,9 @@ import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.powerlevel.PowerLevelsObservableFactory @@ -39,7 +40,7 @@ class RoomPermissionsViewModel @AssistedInject constructor(@Assisted initialStat private val session: Session) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: RoomPermissionsViewState): RoomPermissionsViewModel } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt index 48ff38f92e..bf227ea5e8 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -20,8 +20,9 @@ import androidx.core.net.toFile import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.powerlevel.PowerLevelsObservableFactory @@ -45,7 +46,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: private val session: Session) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: RoomSettingsViewState): RoomSettingsViewModel } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt index b62b633a36..cdf139c7f6 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsViewModel.kt @@ -24,8 +24,9 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.launch @@ -41,7 +42,7 @@ class RoomUploadsViewModel @AssistedInject constructor( private val session: Session ) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: RoomUploadsViewState): RoomUploadsViewModel } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt index 84a419c6c6..fe6dc86165 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt @@ -16,6 +16,7 @@ package im.vector.app.features.roomprofile.uploads.media +import android.os.Build import android.os.Bundle import android.util.DisplayMetrics import android.view.LayoutInflater @@ -78,9 +79,14 @@ class RoomUploadsMediaFragment @Inject constructor( controller.listener = this } + @Suppress("DEPRECATION") private fun getNumberOfColumns(): Int { val displayMetrics = DisplayMetrics() - requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + requireContext().display?.getMetrics(displayMetrics) + } else { + requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) + } return dimensionConverter.pxToDp(displayMetrics.widthPixels) / IMAGE_SIZE_DP } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt index 727a6f765e..c12df073ee 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -311,10 +311,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( } mCrossSigningStatePreference.isVisible = true - if (!vectorPreferences.developerMode()) { - // When not in developer mode, intercept click on this preference - mCrossSigningStatePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { true } - } } private val saveMegolmStartForActivityResult = registerStartForActivityResult { diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountAction.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountAction.kt new file mode 100644 index 0000000000..c3fa844805 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountAction.kt @@ -0,0 +1,28 @@ +/* + * 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.settings.account.deactivation + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class DeactivateAccountAction : VectorViewModelAction { + object TogglePassword : DeactivateAccountAction() + data class DeactivateAccount(val eraseAllData: Boolean) : DeactivateAccountAction() + + object SsoAuthDone: DeactivateAccountAction() + data class PasswordAuthDone(val password: String): DeactivateAccountAction() + object ReAuthCancelled: DeactivateAccountAction() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt index 3d128eb755..2cc80bfa23 100644 --- a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.account.deactivation +import android.app.Activity import android.content.Context import android.os.Bundle import android.view.LayoutInflater @@ -23,16 +24,16 @@ import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import com.airbnb.mvrx.fragmentViewModel -import com.airbnb.mvrx.withState -import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.extensions.exhaustive -import im.vector.app.core.extensions.showPassword +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentDeactivateAccountBinding import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs +import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.settings.VectorSettingsActivity +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import javax.inject.Inject @@ -46,6 +47,25 @@ class DeactivateAccountFragment @Inject constructor( return FragmentDeactivateAccountBinding.inflate(inflater, container, false) } + private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) { + LoginFlowTypes.SSO -> { + viewModel.handle(DeactivateAccountAction.SsoAuthDone) + } + LoginFlowTypes.PASSWORD -> { + val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: "" + viewModel.handle(DeactivateAccountAction.PasswordAuthDone(password)) + } + else -> { + viewModel.handle(DeactivateAccountAction.ReAuthCancelled) + } + } + } else { + viewModel.handle(DeactivateAccountAction.ReAuthCancelled) + } + } + override fun onResume() { super.onResume() (activity as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.deactivate_account_title) @@ -66,59 +86,46 @@ class DeactivateAccountFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setupUi() setupViewListeners() observeViewEvents() } - private fun setupUi() { - views.deactivateAccountPassword.textChanges() - .subscribe { - views.deactivateAccountPasswordTil.error = null - views.deactivateAccountSubmit.isEnabled = it.isNotEmpty() - } - .disposeOnDestroyView() - } - private fun setupViewListeners() { - views.deactivateAccountPasswordReveal.setOnClickListener { - viewModel.handle(DeactivateAccountAction.TogglePassword) - } - views.deactivateAccountSubmit.debouncedClicks { viewModel.handle(DeactivateAccountAction.DeactivateAccount( - views.deactivateAccountPassword.text.toString(), - views.deactivateAccountEraseCheckbox.isChecked)) + views.deactivateAccountEraseCheckbox.isChecked) + ) } } private fun observeViewEvents() { viewModel.observeViewEvents { when (it) { - is DeactivateAccountViewEvents.Loading -> { + is DeactivateAccountViewEvents.Loading -> { settingsActivity?.ignoreInvalidTokenError = true showLoadingDialog(it.message) } - DeactivateAccountViewEvents.EmptyPassword -> { + DeactivateAccountViewEvents.InvalidAuth -> { + dismissLoadingDialog() settingsActivity?.ignoreInvalidTokenError = false - views.deactivateAccountPasswordTil.error = getString(R.string.error_empty_field_your_password) } - DeactivateAccountViewEvents.InvalidPassword -> { - settingsActivity?.ignoreInvalidTokenError = false - views.deactivateAccountPasswordTil.error = getString(R.string.settings_fail_to_update_password_invalid_current_password) - } - is DeactivateAccountViewEvents.OtherFailure -> { + is DeactivateAccountViewEvents.OtherFailure -> { settingsActivity?.ignoreInvalidTokenError = false + dismissLoadingDialog() displayErrorDialog(it.throwable) } - DeactivateAccountViewEvents.Done -> + DeactivateAccountViewEvents.Done -> { MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCredentials = true, isAccountDeactivated = true)) + } + is DeactivateAccountViewEvents.RequestReAuth -> { + ReAuthActivity.newIntent(requireContext(), + it.registrationFlowResponse, + it.lastErrorCode, + getString(R.string.deactivate_account_title)).let { intent -> + reAuthActivityResultLauncher.launch(intent) + } + } }.exhaustive } } - - override fun invalidate() = withState(viewModel) { state -> - views.deactivateAccountPassword.showPassword(state.passwordShown) - views.deactivateAccountPasswordReveal.setImageResource(if (state.passwordShown) R.drawable.ic_eye_closed else R.drawable.ic_eye) - } } diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewEvents.kt index 46acb4aee4..1b0ec2de0c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewEvents.kt @@ -17,14 +17,15 @@ package im.vector.app.features.settings.account.deactivation import im.vector.app.core.platform.VectorViewEvents +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse /** * Transient events for deactivate account settings screen */ sealed class DeactivateAccountViewEvents : VectorViewEvents { data class Loading(val message: CharSequence? = null) : DeactivateAccountViewEvents() - object EmptyPassword : DeactivateAccountViewEvents() - object InvalidPassword : DeactivateAccountViewEvents() + object InvalidAuth : DeactivateAccountViewEvents() data class OtherFailure(val throwable: Throwable) : DeactivateAccountViewEvents() object Done : DeactivateAccountViewEvents() + data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : DeactivateAccountViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt index 211559c657..49cb75c9d6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt @@ -20,38 +20,69 @@ import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel -import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.features.auth.ReAuthActivity import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.failure.isInvalidPassword +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.failure.isInvalidUIAAuth import org.matrix.android.sdk.api.session.Session -import java.lang.Exception +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 +import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth +import org.matrix.android.sdk.api.auth.UIABaseAuth +import org.matrix.android.sdk.api.auth.UserPasswordAuth +import timber.log.Timber +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume data class DeactivateAccountViewState( val passwordShown: Boolean = false ) : MvRxState -sealed class DeactivateAccountAction : VectorViewModelAction { - object TogglePassword : DeactivateAccountAction() - data class DeactivateAccount(val password: String, val eraseAllData: Boolean) : DeactivateAccountAction() -} - class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private val initialState: DeactivateAccountViewState, private val session: Session) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: DeactivateAccountViewState): DeactivateAccountViewModel } + var uiaContinuation: Continuation? = null + var pendingAuth: UIABaseAuth? = null + override fun handle(action: DeactivateAccountAction) { when (action) { - DeactivateAccountAction.TogglePassword -> handleTogglePassword() + DeactivateAccountAction.TogglePassword -> handleTogglePassword() is DeactivateAccountAction.DeactivateAccount -> handleDeactivateAccount(action) + DeactivateAccountAction.SsoAuthDone -> { + Timber.d("## UIA - FallBack success") + if (pendingAuth != null) { + uiaContinuation?.resume(pendingAuth!!) + } else { + uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException()))) + } + } + is DeactivateAccountAction.PasswordAuthDone -> { + val decryptedPass = session.loadSecureSecret(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS) + uiaContinuation?.resume( + UserPasswordAuth( + session = pendingAuth?.session, + password = decryptedPass, + user = session.myUserId + ) + ) + } + DeactivateAccountAction.ReAuthCancelled -> { + Timber.d("## UIA - Reauth cancelled") + uiaContinuation?.resumeWith(Result.failure((Exception()))) + uiaContinuation = null + pendingAuth = null + } }.exhaustive } @@ -62,20 +93,22 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v } private fun handleDeactivateAccount(action: DeactivateAccountAction.DeactivateAccount) { - if (action.password.isEmpty()) { - _viewEvents.post(DeactivateAccountViewEvents.EmptyPassword) - return - } - _viewEvents.post(DeactivateAccountViewEvents.Loading()) viewModelScope.launch { val event = try { - session.deactivateAccount(action.password, action.eraseAllData) + session.deactivateAccount( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + _viewEvents.post(DeactivateAccountViewEvents.RequestReAuth(flowResponse, errCode)) + pendingAuth = DefaultBaseAuth(session = flowResponse.session) + uiaContinuation = promise + } + }, action.eraseAllData) DeactivateAccountViewEvents.Done } catch (failure: Exception) { - if (failure.isInvalidPassword()) { - DeactivateAccountViewEvents.InvalidPassword + if (failure.isInvalidUIAAuth()) { + DeactivateAccountViewEvents.InvalidAuth } else { DeactivateAccountViewEvents.OtherFailure(failure) } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsAction.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsAction.kt index af6ca9f4b7..735c456ff9 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsAction.kt @@ -18,4 +18,9 @@ package im.vector.app.features.settings.crosssigning import im.vector.app.core.platform.VectorViewModelAction -sealed class CrossSigningSettingsAction : VectorViewModelAction +sealed class CrossSigningSettingsAction : VectorViewModelAction { + object InitializeCrossSigning: CrossSigningSettingsAction() + object SsoAuthDone: CrossSigningSettingsAction() + data class PasswordAuthDone(val password: String): CrossSigningSettingsAction() + object ReAuthCancelled: CrossSigningSettingsAction() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsController.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsController.kt index 82279a3906..6425256929 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsController.kt @@ -19,8 +19,11 @@ import com.airbnb.epoxy.TypedEpoxyController import im.vector.app.R import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.list.genericButtonItem import im.vector.app.core.ui.list.genericItem import im.vector.app.core.ui.list.genericItemWithValue +import im.vector.app.core.ui.list.genericPositiveButtonItem +import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.core.utils.DimensionConverter import me.gujun.android.span.span import javax.inject.Inject @@ -31,7 +34,9 @@ class CrossSigningSettingsController @Inject constructor( private val dimensionConverter: DimensionConverter ) : TypedEpoxyController() { - interface InteractionListener + interface InteractionListener { + fun didTapInitializeCrossSigning() + } var interactionListener: InteractionListener? = null @@ -44,6 +49,13 @@ class CrossSigningSettingsController @Inject constructor( titleIconResourceId(R.drawable.ic_shield_trusted) title(stringProvider.getString(R.string.encryption_information_dg_xsigning_complete)) } + genericButtonItem { + id("Reset") + text(stringProvider.getString(R.string.reset_cross_signing)) + buttonClickAction(DebouncedClickListener({ + interactionListener?.didTapInitializeCrossSigning() + })) + } } data.xSigningKeysAreTrusted -> { genericItem { @@ -51,6 +63,13 @@ class CrossSigningSettingsController @Inject constructor( titleIconResourceId(R.drawable.ic_shield_custom) title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted)) } + genericButtonItem { + id("Reset") + text(stringProvider.getString(R.string.reset_cross_signing)) + buttonClickAction(DebouncedClickListener({ + interactionListener?.didTapInitializeCrossSigning() + })) + } } data.xSigningIsEnableInAccount -> { genericItem { @@ -58,12 +77,27 @@ class CrossSigningSettingsController @Inject constructor( titleIconResourceId(R.drawable.ic_shield_black) title(stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted)) } + genericButtonItem { + id("Reset") + text(stringProvider.getString(R.string.reset_cross_signing)) + buttonClickAction(DebouncedClickListener({ + interactionListener?.didTapInitializeCrossSigning() + })) + } } else -> { genericItem { id("not") title(stringProvider.getString(R.string.encryption_information_dg_xsigning_disabled)) } + + genericPositiveButtonItem { + id("Initialize") + text(stringProvider.getString(R.string.initialize_cross_signing)) + buttonClickAction(DebouncedClickListener({ + interactionListener?.didTapInitializeCrossSigning() + })) + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt index 63611efae5..80e44174ff 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt @@ -15,20 +15,26 @@ */ package im.vector.app.features.settings.crosssigning +import android.app.Activity import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.extensions.registerStartForActivityResult +import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentGenericRecyclerBinding +import im.vector.app.features.auth.ReAuthActivity +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import javax.inject.Inject @@ -47,19 +53,55 @@ class CrossSigningSettingsFragment @Inject constructor( private val viewModel: CrossSigningSettingsViewModel by fragmentViewModel() + private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) { + LoginFlowTypes.SSO -> { + viewModel.handle(CrossSigningSettingsAction.SsoAuthDone) + } + LoginFlowTypes.PASSWORD -> { + val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: "" + viewModel.handle(CrossSigningSettingsAction.PasswordAuthDone(password)) + } + else -> { + viewModel.handle(CrossSigningSettingsAction.ReAuthCancelled) + } + } +// activityResult.data?.extras?.getString(ReAuthActivity.RESULT_TOKEN)?.let { token -> +// } + } else { + viewModel.handle(CrossSigningSettingsAction.ReAuthCancelled) + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupRecyclerView() - viewModel.observeViewEvents { - when (it) { + viewModel.observeViewEvents { event -> + when (event) { is CrossSigningSettingsViewEvents.Failure -> { AlertDialog.Builder(requireContext()) .setTitle(R.string.dialog_title_error) - .setMessage(errorFormatter.toHumanReadable(it.throwable)) + .setMessage(errorFormatter.toHumanReadable(event.throwable)) .setPositiveButton(R.string.ok, null) .show() Unit } + is CrossSigningSettingsViewEvents.RequestReAuth -> { + ReAuthActivity.newIntent(requireContext(), + event.registrationFlowResponse, + event.lastErrorCode, + getString(R.string.initialize_cross_signing)).let { intent -> + reAuthActivityResultLauncher.launch(intent) + } + } + is CrossSigningSettingsViewEvents.ShowModalWaitingView -> { + views.waitingView.waitingView.isVisible = true + views.waitingView.waitingStatusText.setTextOrHide(event.status) + } + CrossSigningSettingsViewEvents.HideModalWaitingView -> { + views.waitingView.waitingView.isVisible = false + } }.exhaustive } } @@ -83,4 +125,8 @@ class CrossSigningSettingsFragment @Inject constructor( controller.interactionListener = null super.onDestroyView() } + + override fun didTapInitializeCrossSigning() { + viewModel.handle(CrossSigningSettingsAction.InitializeCrossSigning) + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewEvents.kt index b81a321f3f..1c11560d40 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewEvents.kt @@ -17,10 +17,14 @@ package im.vector.app.features.settings.crosssigning import im.vector.app.core.platform.VectorViewEvents +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse /** * Transient events for cross signing settings screen */ sealed class CrossSigningSettingsViewEvents : VectorViewEvents { data class Failure(val throwable: Throwable) : CrossSigningSettingsViewEvents() + data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : CrossSigningSettingsViewEvents() + data class ShowModalWaitingView(val status: String?) : CrossSigningSettingsViewEvents() + object HideModalWaitingView : CrossSigningSettingsViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt index 3f70ac1fd7..04100056aa 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt @@ -15,24 +15,48 @@ */ package im.vector.app.features.settings.crosssigning +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.R +import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.auth.ReAuthActivity +import im.vector.app.features.login.ReAuthHelper import io.reactivex.Observable import io.reactivex.functions.BiFunction +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +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.LoginFlowTypes +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage import org.matrix.android.sdk.api.session.Session 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.crosssigning.fromBase64 import org.matrix.android.sdk.internal.crypto.crosssigning.isVerified +import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo +import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx +import timber.log.Timber +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume -class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted private val initialState: CrossSigningSettingsViewState, - private val session: Session) - : VectorViewModel(initialState) { +class CrossSigningSettingsViewModel @AssistedInject constructor( + @Assisted private val initialState: CrossSigningSettingsViewState, + private val session: Session, + private val reAuthHelper: ReAuthHelper, + private val stringProvider: StringProvider +) : VectorViewModel(initialState) { init { Observable.combineLatest, Optional, Pair, Optional>>( @@ -57,15 +81,82 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat } } - @AssistedInject.Factory + var uiaContinuation: Continuation? = null + var pendingAuth: UIABaseAuth? = null + + @AssistedFactory interface Factory { fun create(initialState: CrossSigningSettingsViewState): CrossSigningSettingsViewModel } override fun handle(action: CrossSigningSettingsAction) { - // No op for the moment - // when (action) { - // }.exhaustive + when (action) { + CrossSigningSettingsAction.InitializeCrossSigning -> { + _viewEvents.post(CrossSigningSettingsViewEvents.ShowModalWaitingView(null)) + viewModelScope.launch(Dispatchers.IO) { + try { + awaitCallback { + session.cryptoService().crossSigningService().initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, + errCode: String?, + promise: Continuation) { + Timber.d("## UIA : initializeCrossSigning UIA") + if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD + && reAuthHelper.data != null && errCode == null) { + UserPasswordAuth( + session = null, + user = session.myUserId, + password = reAuthHelper.data + ).let { promise.resume(it) } + } else { + Timber.d("## UIA : initializeCrossSigning UIA > start reauth activity") + _viewEvents.post(CrossSigningSettingsViewEvents.RequestReAuth(flowResponse, errCode)) + pendingAuth = DefaultBaseAuth(session = flowResponse.session) + uiaContinuation = promise + } + } + }, it) + } + } catch (failure: Throwable) { + handleInitializeXSigningError(failure) + } finally { + _viewEvents.post(CrossSigningSettingsViewEvents.HideModalWaitingView) + } + } + Unit + } + is CrossSigningSettingsAction.SsoAuthDone -> { + Timber.d("## UIA - FallBack success") + if (pendingAuth != null) { + uiaContinuation?.resume(pendingAuth!!) + } else { + uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException()))) + } + } + is CrossSigningSettingsAction.PasswordAuthDone -> { + val decryptedPass = session.loadSecureSecret(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS) + uiaContinuation?.resume( + UserPasswordAuth( + session = pendingAuth?.session, + password = decryptedPass, + user = session.myUserId + ) + ) + } + CrossSigningSettingsAction.ReAuthCancelled -> { + Timber.d("## UIA - Reauth cancelled") + _viewEvents.post(CrossSigningSettingsViewEvents.HideModalWaitingView) + uiaContinuation?.resumeWith(Result.failure((Exception()))) + uiaContinuation = null + pendingAuth = null + } + }.exhaustive + } + + private fun handleInitializeXSigningError(failure: Throwable) { + Timber.e(failure, "## CrossSigning - Failed to initialize cross signing") + _viewEvents.post(CrossSigningSettingsViewEvents.Failure(Exception(stringProvider.getString(R.string.failed_to_initialize_cross_signing)))) } companion object : MvRxViewModelFactory { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt index ad840d2efb..ee5b0a6092 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt @@ -19,8 +19,9 @@ import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -33,7 +34,7 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As val session: Session ) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: DeviceVerificationInfoBottomSheetViewState, deviceId: String): DeviceVerificationInfoBottomSheetViewModel } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesAction.kt index 2b0991ab4e..46a476c270 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesAction.kt @@ -22,7 +22,7 @@ import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo sealed class DevicesAction : VectorViewModelAction { object Refresh : DevicesAction() data class Delete(val deviceId: String) : DevicesAction() - data class Password(val password: String) : DevicesAction() +// data class Password(val password: String) : DevicesAction() data class Rename(val deviceId: String, val newName: String) : DevicesAction() data class PromptRename(val deviceId: String) : DevicesAction() @@ -30,4 +30,8 @@ sealed class DevicesAction : VectorViewModelAction { data class VerifyMyDeviceManually(val deviceId: String) : DevicesAction() object CompleteSecurity : DevicesAction() data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction() + + object SsoAuthDone: DevicesAction() + data class PasswordAuthDone(val password: String): DevicesAction() + object ReAuthCancelled: DevicesAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt index 60d7491603..8535c698a7 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt @@ -19,6 +19,7 @@ package im.vector.app.features.settings.devices import im.vector.app.core.platform.VectorViewEvents import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo @@ -27,9 +28,12 @@ import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo */ sealed class DevicesViewEvents : VectorViewEvents { data class Loading(val message: CharSequence? = null) : DevicesViewEvents() +// object HideLoading : DevicesViewEvents() data class Failure(val throwable: Throwable) : DevicesViewEvents() - object RequestPassword : DevicesViewEvents() +// object RequestPassword : DevicesViewEvents() + + data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : DevicesViewEvents() data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt index 8c5762afce..b91b5255b6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt @@ -26,16 +26,22 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject -import im.vector.app.core.error.SsoFlowNotSupportedYet +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.R import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.auth.ReAuthActivity +import im.vector.app.features.login.ReAuthHelper import io.reactivex.Observable import io.reactivex.functions.BiFunction import io.reactivex.subjects.PublishSubject +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch 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.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.Session @@ -43,13 +49,22 @@ 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.api.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo +import org.matrix.android.sdk.api.auth.UIABaseAuth +import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx import timber.log.Timber import java.util.concurrent.TimeUnit +import javax.net.ssl.HttpsURLConnection +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume data class DevicesViewState( val myDeviceId: String = "", @@ -69,10 +84,15 @@ data class DeviceFullInfo( class DevicesViewModel @AssistedInject constructor( @Assisted initialState: DevicesViewState, - private val session: Session + private val session: Session, + private val reAuthHelper: ReAuthHelper, + private val stringProvider: StringProvider ) : VectorViewModel(initialState), VerificationService.Listener { - @AssistedInject.Factory + var uiaContinuation: Continuation? = null + var pendingAuth: UIABaseAuth? = null + + @AssistedFactory interface Factory { fun create(initialState: DevicesViewState): DevicesViewModel } @@ -86,10 +106,6 @@ class DevicesViewModel @AssistedInject constructor( } } - // temp storage when we ask for the user password - private var _currentDeviceId: String? = null - private var _currentSession: String? = null - private val refreshPublisher: PublishSubject = PublishSubject.create() init { @@ -188,13 +204,43 @@ class DevicesViewModel @AssistedInject constructor( return when (action) { is DevicesAction.Refresh -> queryRefreshDevicesList() is DevicesAction.Delete -> handleDelete(action) - is DevicesAction.Password -> handlePassword(action) is DevicesAction.Rename -> handleRename(action) is DevicesAction.PromptRename -> handlePromptRename(action) is DevicesAction.VerifyMyDevice -> handleInteractiveVerification(action) is DevicesAction.CompleteSecurity -> handleCompleteSecurity() is DevicesAction.MarkAsManuallyVerified -> handleVerifyManually(action) is DevicesAction.VerifyMyDeviceManually -> handleShowDeviceCryptoInfo(action) + is DevicesAction.SsoAuthDone -> { + // we should use token based auth + // _viewEvents.post(CrossSigningSettingsViewEvents.ShowModalWaitingView(null)) + // will release the interactive auth interceptor + Timber.d("## UIA - FallBack success $pendingAuth , continuation: $uiaContinuation") + if (pendingAuth != null) { + uiaContinuation?.resume(pendingAuth!!) + } else { + uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException()))) + } + Unit + } + is DevicesAction.PasswordAuthDone -> { + val decryptedPass = session.loadSecureSecret(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS) + uiaContinuation?.resume( + UserPasswordAuth( + session = pendingAuth?.session, + password = decryptedPass, + user = session.myUserId + ) + ) + Unit + } + DevicesAction.ReAuthCancelled -> { + Timber.d("## UIA - Reauth cancelled") +// _viewEvents.post(DevicesViewEvents.Loading) + uiaContinuation?.resumeWith(Result.failure((Exception()))) + uiaContinuation = null + pendingAuth = null + Unit + } } } @@ -284,95 +330,48 @@ class DevicesViewModel @AssistedInject constructor( ) } - session.cryptoService().deleteDevice(deviceId, object : MatrixCallback { - override fun onFailure(failure: Throwable) { - var isPasswordRequestFound = false - - if (failure is Failure.RegistrationFlowError) { - // We only support LoginFlowTypes.PASSWORD - // Check if we can provide the user password - failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow -> - isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true - } - - if (isPasswordRequestFound) { - _currentDeviceId = deviceId - _currentSession = failure.registrationFlowResponse.session - - setState { - copy( - request = Success(Unit) - ) + viewModelScope.launch(Dispatchers.IO) { + try { + awaitCallback { + session.cryptoService().deleteDevice(deviceId, object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + Timber.d("## UIA : deleteDevice UIA") + if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && reAuthHelper.data != null && errCode == null) { + UserPasswordAuth( + session = null, + user = session.myUserId, + password = reAuthHelper.data + ).let { promise.resume(it) } + } else { + Timber.d("## UIA : deleteDevice UIA > start reauth activity") + _viewEvents.post(DevicesViewEvents.RequestReAuth(flowResponse, errCode)) + pendingAuth = DefaultBaseAuth(session = flowResponse.session) + uiaContinuation = promise + } } - - _viewEvents.post(DevicesViewEvents.RequestPassword) - } + }, it) } - - if (!isPasswordRequestFound) { - // LoginFlowTypes.PASSWORD not supported, and this is the only one Element supports so far... - setState { - copy( - request = Fail(failure) - ) - } - - _viewEvents.post(DevicesViewEvents.Failure(SsoFlowNotSupportedYet())) - } - } - - override fun onSuccess(data: Unit) { setState { copy( - request = Success(data) + request = Success(Unit) ) } // force settings update queryRefreshDevicesList() - } - }) - } - - private fun handlePassword(action: DevicesAction.Password) { - val currentDeviceId = _currentDeviceId - if (currentDeviceId.isNullOrBlank()) { - // Abort - return - } - - setState { - copy( - request = Loading() - ) - } - - session.cryptoService().deleteDeviceWithUserPassword(currentDeviceId, _currentSession, action.password, object : MatrixCallback { - override fun onSuccess(data: Unit) { - _currentDeviceId = null - _currentSession = null - - setState { - copy( - request = Success(data) - ) - } - // force settings update - queryRefreshDevicesList() - } - - override fun onFailure(failure: Throwable) { - _currentDeviceId = null - _currentSession = null - - // Password is maybe not good + } catch (failure: Throwable) { setState { copy( request = Fail(failure) ) } - - _viewEvents.post(DevicesViewEvents.Failure(failure)) + if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) { + _viewEvents.post(DevicesViewEvents.Failure(Exception(stringProvider.getString(R.string.authentication_error)))) + } else { + _viewEvents.post(DevicesViewEvents.Failure(Exception(stringProvider.getString(R.string.matrix_error)))) + } + // ... + Timber.e(failure, "failed to delete session") } - }) + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt index 1bf538d458..0040f73fec 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.devices +import android.app.Activity import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -29,14 +30,16 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.dialogs.ManuallyVerifyDialog -import im.vector.app.core.dialogs.PromptPasswordDialog import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.DialogBaseEditTextBinding import im.vector.app.databinding.FragmentGenericRecyclerBinding +import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.crypto.verification.VerificationBottomSheet +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import javax.inject.Inject @@ -52,7 +55,7 @@ class VectorSettingsDevicesFragment @Inject constructor( // used to avoid requesting to enter the password for each deletion // Note: Sonar does not like to use password for member name. - private var mAccountPass: String = "" +// private var mAccountPass: String = "" override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentGenericRecyclerBinding { return FragmentGenericRecyclerBinding.inflate(inflater, container, false) @@ -71,7 +74,7 @@ class VectorSettingsDevicesFragment @Inject constructor( when (it) { is DevicesViewEvents.Loading -> showLoading(it.message) is DevicesViewEvents.Failure -> showFailure(it.throwable) - is DevicesViewEvents.RequestPassword -> maybeShowDeleteDeviceWithPasswordDialog() + is DevicesViewEvents.RequestReAuth -> askForReAuthentication(it) is DevicesViewEvents.PromptRenameDevice -> displayDeviceRenameDialog(it.deviceInfo) is DevicesViewEvents.ShowVerifyDevice -> { VerificationBottomSheet.withArgs( @@ -93,13 +96,6 @@ class VectorSettingsDevicesFragment @Inject constructor( } } - override fun showFailure(throwable: Throwable) { - super.showFailure(throwable) - - // Password is maybe not good, for safety measure, reset it here - mAccountPass = "" - } - override fun onDestroyView() { devicesController.callback = null views.genericRecyclerView.cleanup() @@ -119,14 +115,6 @@ class VectorSettingsDevicesFragment @Inject constructor( ) } -// override fun onDeleteDevice(deviceInfo: DeviceInfo) { -// devicesViewModel.handle(DevicesAction.Delete(deviceInfo)) -// } -// -// override fun onRenameDevice(deviceInfo: DeviceInfo) { -// displayDeviceRenameDialog(deviceInfo) -// } - override fun retry() { viewModel.handle(DevicesAction.Refresh) } @@ -154,17 +142,34 @@ class VectorSettingsDevicesFragment @Inject constructor( .show() } - /** - * Show a dialog to ask for user password, or use a previously entered password. - */ - private fun maybeShowDeleteDeviceWithPasswordDialog() { - if (mAccountPass.isNotEmpty()) { - viewModel.handle(DevicesAction.Password(mAccountPass)) - } else { - PromptPasswordDialog().show(requireActivity()) { password -> - mAccountPass = password - viewModel.handle(DevicesAction.Password(mAccountPass)) + private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) { + LoginFlowTypes.SSO -> { + viewModel.handle(DevicesAction.SsoAuthDone) + } + LoginFlowTypes.PASSWORD -> { + val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: "" + viewModel.handle(DevicesAction.PasswordAuthDone(password)) + } + else -> { + viewModel.handle(DevicesAction.ReAuthCancelled) + } } + } else { + viewModel.handle(DevicesAction.ReAuthCancelled) + } + } + + /** + * Launch the re auth activity to get credentials + */ + private fun askForReAuthentication(reAuthReq: DevicesViewEvents.RequestReAuth) { + ReAuthActivity.newIntent(requireContext(), + reAuthReq.registrationFlowResponse, + reAuthReq.lastErrorCode, + getString(R.string.devices_delete_dialog_title)).let { intent -> + reAuthActivityResultLauncher.launch(intent) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataViewModel.kt index af8d162f15..b2200e6a6d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/AccountDataViewModel.kt @@ -23,8 +23,9 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -63,7 +64,7 @@ class AccountDataViewModel @AssistedInject constructor(@Assisted initialState: A } } - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: AccountDataViewState): AccountDataViewModel } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt index 4249ef09fa..325538ee5e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt @@ -24,8 +24,9 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -57,7 +58,7 @@ class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted i override fun handle(action: EmptyAction) {} - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: GossipingEventsPaperTrailState): GossipingEventsPaperTrailViewModel } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt index 0b0b923a48..c0a791233f 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt @@ -24,8 +24,9 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -65,7 +66,7 @@ class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState override fun handle(action: EmptyAction) {} - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: KeyRequestListViewState): KeyRequestListViewModel } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt index a7d5e8f1ac..e7a56ef9df 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt @@ -27,8 +27,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewModel @@ -54,7 +55,7 @@ class KeyRequestViewModel @AssistedInject constructor( private val session: Session) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: KeyRequestViewState): KeyRequestViewModel } diff --git a/vector/src/main/java/im/vector/app/features/settings/ignored/IgnoredUsersViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/ignored/IgnoredUsersViewModel.kt index 98c9f8e4f1..fdc6585829 100644 --- a/vector/src/main/java/im/vector/app/features/settings/ignored/IgnoredUsersViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/ignored/IgnoredUsersViewModel.kt @@ -25,8 +25,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModelAction import org.matrix.android.sdk.api.MatrixCallback @@ -47,7 +48,7 @@ class IgnoredUsersViewModel @AssistedInject constructor(@Assisted initialState: private val session: Session) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: IgnoredUsersViewState): IgnoredUsersViewModel } diff --git a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewModel.kt index df7cc4ba84..2e59b0ef7d 100644 --- a/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/locale/LocalePickerViewModel.kt @@ -22,8 +22,9 @@ import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.configuration.VectorConfiguration @@ -35,7 +36,7 @@ class LocalePickerViewModel @AssistedInject constructor( private val vectorConfiguration: VectorConfiguration ) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: LocalePickerViewState): LocalePickerViewModel } diff --git a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysViewModel.kt index 2c8b902188..7981d71ce1 100644 --- a/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/push/PushGatewaysViewModel.kt @@ -22,8 +22,9 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -39,7 +40,7 @@ class PushGatewaysViewModel @AssistedInject constructor(@Assisted initialState: private val session: Session) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: PushGatewayViewState): PushGatewaysViewModel } diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsAction.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsAction.kt index 0be3c6a198..d223009e69 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsAction.kt @@ -25,6 +25,11 @@ sealed class ThreePidsSettingsAction : VectorViewModelAction { data class SubmitCode(val threePid: ThreePid.Msisdn, val code: String) : ThreePidsSettingsAction() data class ContinueThreePid(val threePid: ThreePid) : ThreePidsSettingsAction() data class CancelThreePid(val threePid: ThreePid) : ThreePidsSettingsAction() - data class AccountPassword(val password: String) : ThreePidsSettingsAction() + + // data class AccountPassword(val password: String) : ThreePidsSettingsAction() data class DeleteThreePid(val threePid: ThreePid) : ThreePidsSettingsAction() + + object SsoAuthDone : ThreePidsSettingsAction() + data class PasswordAuthDone(val password: String) : ThreePidsSettingsAction() + object ReAuthCancelled : ThreePidsSettingsAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt index d6da04affc..0a7489e2cc 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.threepids +import android.app.Activity import android.content.DialogInterface import android.os.Bundle import android.view.LayoutInflater @@ -26,7 +27,6 @@ import androidx.appcompat.app.AppCompatActivity import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.app.R -import im.vector.app.core.dialogs.PromptPasswordDialog import im.vector.app.core.dialogs.withColoredButton import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith @@ -35,10 +35,12 @@ import im.vector.app.core.extensions.getFormattedValue import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.isMsisdn +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentGenericRecyclerBinding - +import im.vector.app.features.auth.ReAuthActivity +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.session.identity.ThreePid import javax.inject.Inject @@ -64,15 +66,42 @@ class ThreePidsSettingsFragment @Inject constructor( viewModel.observeViewEvents { when (it) { - is ThreePidsSettingsViewEvents.Failure -> displayErrorDialog(it.throwable) - ThreePidsSettingsViewEvents.RequestPassword -> askUserPassword() + is ThreePidsSettingsViewEvents.Failure -> displayErrorDialog(it.throwable) + is ThreePidsSettingsViewEvents.RequestReAuth -> askAuthentication(it) }.exhaustive } } - private fun askUserPassword() { - PromptPasswordDialog().show(requireActivity()) { password -> - viewModel.handle(ThreePidsSettingsAction.AccountPassword(password)) + // private fun askUserPassword() { +// PromptPasswordDialog().show(requireActivity()) { password -> +// viewModel.handle(ThreePidsSettingsAction.AccountPassword(password)) +// } +// } + + private fun askAuthentication(event: ThreePidsSettingsViewEvents.RequestReAuth) { + ReAuthActivity.newIntent(requireContext(), + event.registrationFlowResponse, + event.lastErrorCode, + getString(R.string.settings_add_email_address)).let { intent -> + reAuthActivityResultLauncher.launch(intent) + } + } + private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) { + LoginFlowTypes.SSO -> { + viewModel.handle(ThreePidsSettingsAction.SsoAuthDone) + } + LoginFlowTypes.PASSWORD -> { + val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: "" + viewModel.handle(ThreePidsSettingsAction.PasswordAuthDone(password)) + } + else -> { + viewModel.handle(ThreePidsSettingsAction.ReAuthCancelled) + } + } + } else { + viewModel.handle(ThreePidsSettingsAction.ReAuthCancelled) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewEvents.kt index 1ac2d10458..0346fd137e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewEvents.kt @@ -17,8 +17,10 @@ package im.vector.app.features.settings.threepids import im.vector.app.core.platform.VectorViewEvents +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse sealed class ThreePidsSettingsViewEvents : VectorViewEvents { data class Failure(val throwable: Throwable) : ThreePidsSettingsViewEvents() - object RequestPassword : ThreePidsSettingsViewEvents() +// object RequestPassword : ThreePidsSettingsViewEvents() + data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : ThreePidsSettingsViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt index 41b6146bb1..a1d4d6227b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt @@ -23,21 +23,29 @@ import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.R -import im.vector.app.core.error.SsoFlowNotSupportedYet import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.ReadOnceTrue +import im.vector.app.features.auth.ReAuthActivity import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.auth.data.LoginFlowTypes -import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 +import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth +import org.matrix.android.sdk.api.auth.UIABaseAuth +import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.rx.rx +import timber.log.Timber +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume class ThreePidsSettingsViewModel @AssistedInject constructor( @Assisted initialState: ThreePidsSettingsViewState, @@ -47,36 +55,16 @@ class ThreePidsSettingsViewModel @AssistedInject constructor( // UIA session private var pendingThreePid: ThreePid? = null - private var pendingSession: String? = null +// private var pendingSession: String? = null private val loadingCallback: MatrixCallback = object : MatrixCallback { override fun onFailure(failure: Throwable) { isLoading(false) - - if (failure is Failure.RegistrationFlowError) { - var isPasswordRequestFound = false - - // We only support LoginFlowTypes.PASSWORD - // Check if we can provide the user password - failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow -> - isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true - } - - if (isPasswordRequestFound) { - pendingSession = failure.registrationFlowResponse.session - _viewEvents.post(ThreePidsSettingsViewEvents.RequestPassword) - } else { - // LoginFlowTypes.PASSWORD not supported, and this is the only one Element supports so far... - _viewEvents.post(ThreePidsSettingsViewEvents.Failure(SsoFlowNotSupportedYet())) - } - } else { - _viewEvents.post(ThreePidsSettingsViewEvents.Failure(failure)) - } + _viewEvents.post(ThreePidsSettingsViewEvents.Failure(failure)) } override fun onSuccess(data: Unit) { pendingThreePid = null - pendingSession = null isLoading(false) } } @@ -89,7 +77,7 @@ class ThreePidsSettingsViewModel @AssistedInject constructor( } } - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: ThreePidsSettingsViewState): ThreePidsSettingsViewModel } @@ -141,16 +129,50 @@ class ThreePidsSettingsViewModel @AssistedInject constructor( override fun handle(action: ThreePidsSettingsAction) { when (action) { - is ThreePidsSettingsAction.AddThreePid -> handleAddThreePid(action) + is ThreePidsSettingsAction.AddThreePid -> handleAddThreePid(action) is ThreePidsSettingsAction.ContinueThreePid -> handleContinueThreePid(action) - is ThreePidsSettingsAction.SubmitCode -> handleSubmitCode(action) - is ThreePidsSettingsAction.CancelThreePid -> handleCancelThreePid(action) - is ThreePidsSettingsAction.AccountPassword -> handleAccountPassword(action) - is ThreePidsSettingsAction.DeleteThreePid -> handleDeleteThreePid(action) - is ThreePidsSettingsAction.ChangeUiState -> handleChangeUiState(action) + is ThreePidsSettingsAction.SubmitCode -> handleSubmitCode(action) + is ThreePidsSettingsAction.CancelThreePid -> handleCancelThreePid(action) + is ThreePidsSettingsAction.DeleteThreePid -> handleDeleteThreePid(action) + is ThreePidsSettingsAction.ChangeUiState -> handleChangeUiState(action) + ThreePidsSettingsAction.SsoAuthDone -> { + Timber.d("## UIA - FallBack success") + if (pendingAuth != null) { + uiaContinuation?.resume(pendingAuth!!) + } else { + uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException()))) + } + } + is ThreePidsSettingsAction.PasswordAuthDone -> { + val decryptedPass = session.loadSecureSecret(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS) + uiaContinuation?.resume( + UserPasswordAuth( + session = pendingAuth?.session, + password = decryptedPass, + user = session.myUserId + ) + ) + } + ThreePidsSettingsAction.ReAuthCancelled -> { + Timber.d("## UIA - Reauth cancelled") + uiaContinuation?.resumeWith(Result.failure((Exception()))) + uiaContinuation = null + pendingAuth = null + } }.exhaustive } + var uiaContinuation: Continuation? = null + var pendingAuth: UIABaseAuth? = null + + private val uiaInterceptor = object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + _viewEvents.post(ThreePidsSettingsViewEvents.RequestReAuth(flowResponse, errCode)) + pendingAuth = DefaultBaseAuth(session = flowResponse.session) + uiaContinuation = promise + } + } + private fun handleSubmitCode(action: ThreePidsSettingsAction.SubmitCode) { isLoading(true) setState { @@ -167,7 +189,7 @@ class ThreePidsSettingsViewModel @AssistedInject constructor( override fun onSuccess(data: Unit) { // then finalize pendingThreePid = action.threePid - session.finalizeAddingThreePid(action.threePid, null, null, loadingCallback) + session.finalizeAddingThreePid(action.threePid, uiaInterceptor, loadingCallback) } override fun onFailure(failure: Throwable) { @@ -231,7 +253,7 @@ class ThreePidsSettingsViewModel @AssistedInject constructor( isLoading(true) pendingThreePid = action.threePid viewModelScope.launch { - session.finalizeAddingThreePid(action.threePid, null, null, loadingCallback) + session.finalizeAddingThreePid(action.threePid, uiaInterceptor, loadingCallback) } } @@ -242,16 +264,14 @@ class ThreePidsSettingsViewModel @AssistedInject constructor( } } - private fun handleAccountPassword(action: ThreePidsSettingsAction.AccountPassword) { - val safeSession = pendingSession ?: return Unit - .also { _viewEvents.post(ThreePidsSettingsViewEvents.Failure(IllegalStateException("No pending session"))) } - val safeThreePid = pendingThreePid ?: return Unit - .also { _viewEvents.post(ThreePidsSettingsViewEvents.Failure(IllegalStateException("No pending threePid"))) } - isLoading(true) - viewModelScope.launch { - session.finalizeAddingThreePid(safeThreePid, safeSession, action.password, loadingCallback) - } - } +// private fun handleAccountPassword(action: ThreePidsSettingsAction.AccountPassword) { +// val safeThreePid = pendingThreePid ?: return Unit +// .also { _viewEvents.post(ThreePidsSettingsViewEvents.Failure(IllegalStateException("No pending threePid"))) } +// isLoading(true) +// viewModelScope.launch { +// session.finalizeAddingThreePid(safeThreePid, uiaInterceptor, loadingCallback) +// } +// } private fun handleDeleteThreePid(action: ThreePidsSettingsAction.DeleteThreePid) { isLoading(true) diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareViewModel.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareViewModel.kt index 9014565e08..4d211d11de 100644 --- a/vector/src/main/java/im/vector/app/features/share/IncomingShareViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareViewModel.kt @@ -20,8 +20,9 @@ import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import com.jakewharton.rxrelay2.BehaviorRelay -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.toggle import im.vector.app.core.platform.VectorViewModel @@ -42,7 +43,7 @@ class IncomingShareViewModel @AssistedInject constructor( private val breadcrumbsRoomComparator: BreadcrumbsRoomComparator) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: IncomingShareViewState): IncomingShareViewModel } diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt index 81ac1dbd54..98ec2d07de 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt @@ -16,6 +16,7 @@ package im.vector.app.features.signout.soft +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading @@ -23,12 +24,14 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.hasUnsavedKeys import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.login.LoginMode +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.data.LoginFlowResult @@ -47,7 +50,7 @@ class SoftLogoutViewModel @AssistedInject constructor( private val authenticationService: AuthenticationService ) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: SoftLogoutViewState): SoftLogoutViewModel } @@ -173,22 +176,19 @@ class SoftLogoutViewModel @AssistedInject constructor( asyncLoginAction = Loading() ) } - currentTask = session.updateCredentials(action.credentials, - object : MatrixCallback { - override fun onFailure(failure: Throwable) { - _viewEvents.post(SoftLogoutViewEvents.Failure(failure)) - setState { - copy( - asyncLoginAction = Uninitialized - ) - } - } - - override fun onSuccess(data: Unit) { - onSessionRestored() - } + viewModelScope.launch { + try { + session.updateCredentials(action.credentials) + onSessionRestored() + } catch (failure: Throwable) { + _viewEvents.post(SoftLogoutViewEvents.Failure(failure)) + setState { + copy( + asyncLoginAction = Uninitialized + ) } - ) + } + } } } } @@ -201,21 +201,18 @@ class SoftLogoutViewModel @AssistedInject constructor( passwordShown = false ) } - currentTask = session.signInAgain(action.password, - object : MatrixCallback { - override fun onFailure(failure: Throwable) { - setState { - copy( - asyncLoginAction = Fail(failure) - ) - } - } - - override fun onSuccess(data: Unit) { - onSessionRestored() - } + viewModelScope.launch { + try { + session.signInAgain(action.password) + onSessionRestored() + } catch (failure: Throwable) { + setState { + copy( + asyncLoginAction = Fail(failure) + ) } - ) + } + } } private fun onSessionRestored() { diff --git a/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt b/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt index 89d6e970cc..4ecad80876 100644 --- a/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/terms/ReviewTermsViewModel.kt @@ -22,8 +22,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.launch @@ -35,7 +36,7 @@ class ReviewTermsViewModel @AssistedInject constructor( private val session: Session ) : VectorViewModel(initialState) { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: ReviewTermsViewState): ReviewTermsViewModel } diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt index 45b6f0ee65..4caa75b39c 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt @@ -21,8 +21,9 @@ import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.R import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider @@ -66,7 +67,7 @@ class UserCodeSharedViewModel @AssistedInject constructor( } } - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: UserCodeState): UserCodeSharedViewModel } diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt index f8eabbaed0..9766d640c7 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt @@ -21,8 +21,9 @@ import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import com.jakewharton.rxrelay2.BehaviorRelay -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.toggle import im.vector.app.core.platform.VectorViewModel @@ -51,7 +52,7 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User private var currentUserSearchDisposable: Disposable? = null - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: UserListViewState): UserListViewModel } diff --git a/vector/src/main/java/im/vector/app/features/webview/VectorWebViewActivity.kt b/vector/src/main/java/im/vector/app/features/webview/VectorWebViewActivity.kt index 68c13c300e..e4d2571333 100644 --- a/vector/src/main/java/im/vector/app/features/webview/VectorWebViewActivity.kt +++ b/vector/src/main/java/im/vector/app/features/webview/VectorWebViewActivity.kt @@ -64,7 +64,9 @@ class VectorWebViewActivity : VectorBaseActivity() // Allow use of Local Storage domStorageEnabled = true + @Suppress("DEPRECATION") allowFileAccessFromFileURLs = true + @Suppress("DEPRECATION") allowUniversalAccessFromFileURLs = true displayZoomControls = false @@ -73,7 +75,7 @@ class VectorWebViewActivity : VectorBaseActivity() val cookieManager = android.webkit.CookieManager.getInstance() cookieManager.setAcceptThirdPartyCookies(views.simpleWebview, true) - val url = intent.extras?.getString(EXTRA_URL) + val url = intent.extras?.getString(EXTRA_URL) ?: return val title = intent.extras?.getString(EXTRA_TITLE, USE_TITLE_FROM_WEB_PAGE) if (title != USE_TITLE_FROM_WEB_PAGE) { setTitle(title) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt index 3906ea687c..13d49eb20b 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt @@ -17,8 +17,9 @@ package im.vector.app.features.widgets import android.text.TextUtils -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.R import im.vector.app.core.resources.StringProvider import kotlinx.coroutines.GlobalScope @@ -45,7 +46,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo private val stringProvider: StringProvider, private val session: Session) : WidgetPostAPIMediator.Handler { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(roomId: String): WidgetPostAPIHandler } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt index 1bc10e26d4..203b58f68f 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt @@ -25,8 +25,9 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.widgets.permissions.WidgetPermissionsHelper @@ -55,7 +56,7 @@ class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: Wi WidgetPostAPIHandler.NavigationCallback, IntegrationManagerService.Listener { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: WidgetViewState): WidgetViewModel } diff --git a/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt b/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt index eb588ec9ae..3accc56680 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt @@ -20,8 +20,9 @@ import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.R import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.launch @@ -141,7 +142,7 @@ class RoomWidgetPermissionViewModel @AssistedInject constructor(@Assisted val in } } - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: RoomWidgetPermissionViewState): RoomWidgetPermissionViewModel } diff --git a/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt b/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt index 446bc1663f..d30baef55a 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt @@ -54,7 +54,9 @@ fun WebView.setupForWidget(webViewEventListener: WebViewEventListener) { // Allow use of Local Storage settings.domStorageEnabled = true + @Suppress("DEPRECATION") settings.allowFileAccessFromFileURLs = true + @Suppress("DEPRECATION") settings.allowUniversalAccessFromFileURLs = true settings.displayZoomControls = false @@ -75,7 +77,6 @@ fun WebView.clearAfterWidget() { // Make sure you remove the WebView from its parent view before doing anything. (parent as? ViewGroup)?.removeAllViews() webChromeClient = null - webViewClient = null clearHistory() // NOTE: clears RAM cache, if you pass true, it will also clear the disk cache. clearCache(true) diff --git a/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt b/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt index 68f62cffe5..1c3ad7563c 100644 --- a/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt @@ -24,8 +24,9 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel @@ -69,7 +70,7 @@ class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialS private val session: Session) : VectorViewModel(initialState), KeysBackupStateListener { - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel } @@ -114,8 +115,10 @@ class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialS // So recovery is not setup // Check if cross signing is enabled and local secrets known - if (crossSigningInfo.getOrNull()?.isTrusted() == true - && pInfo.getOrNull()?.allKnown().orFalse() + if ( + crossSigningInfo.getOrNull() == null + || (crossSigningInfo.getOrNull()?.isTrusted() == true + && pInfo.getOrNull()?.allKnown().orFalse()) ) { // So 4S is not setup and we have local secrets, return@Function4 BannerState.Setup(numberOfKeys = getNumberOfKeysToBackup()) diff --git a/vector/src/main/java/im/vector/app/features/workers/signout/SignoutCheckViewModel.kt b/vector/src/main/java/im/vector/app/features/workers/signout/SignoutCheckViewModel.kt index 8a1055e780..2f8d45043b 100644 --- a/vector/src/main/java/im/vector/app/features/workers/signout/SignoutCheckViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/workers/signout/SignoutCheckViewModel.kt @@ -26,8 +26,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext -import com.squareup.inject.assisted.Assisted -import com.squareup.inject.assisted.AssistedInject +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.assisted.AssistedFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewModel @@ -63,7 +64,7 @@ class SignoutCheckViewModel @AssistedInject constructor(@Assisted initialState: data class ExportKeys(val exporter: KeysExporter, val passphrase: String, val uri: Uri) : ViewEvents() } - @AssistedInject.Factory + @AssistedFactory interface Factory { fun create(initialState: SignoutCheckViewState): SignoutCheckViewModel } diff --git a/vector/src/main/res/drawable/ic_social_gitlab.xml b/vector/src/main/res/drawable/ic_social_gitlab.xml new file mode 100644 index 0000000000..9399f6448a --- /dev/null +++ b/vector/src/main/res/drawable/ic_social_gitlab.xml @@ -0,0 +1,48 @@ + + + + + + + + + diff --git a/vector/src/main/res/layout/activity_home.xml b/vector/src/main/res/layout/activity_home.xml index 50fc11500a..a41256fb84 100644 --- a/vector/src/main/res/layout/activity_home.xml +++ b/vector/src/main/res/layout/activity_home.xml @@ -25,7 +25,7 @@ diff --git a/vector/src/main/res/layout/fragment_bootstrap_reauth.xml b/vector/src/main/res/layout/fragment_bootstrap_reauth.xml new file mode 100644 index 0000000000..1bc6725c64 --- /dev/null +++ b/vector/src/main/res/layout/fragment_bootstrap_reauth.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_deactivate_account.xml b/vector/src/main/res/layout/fragment_deactivate_account.xml index db85c607e1..4bbf0a496c 100644 --- a/vector/src/main/res/layout/fragment_deactivate_account.xml +++ b/vector/src/main/res/layout/fragment_deactivate_account.xml @@ -31,75 +31,14 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/deactivateAccountContent" /> - - - - - - - - - - - - - - -