From 23623b8895958acfe9b8d6e8b5b674e7d012fe33 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 4 Jan 2021 17:12:49 +0300 Subject: [PATCH 01/51] Migrate to Android 11, API 30. --- CHANGES.md | 4 +- attachment-viewer/build.gradle | 4 +- .../AttachmentViewerActivity.kt | 69 ++- .../src/main/res/values/colors.xml | 6 + matrix-sdk-android-rx/build.gradle | 4 +- matrix-sdk-android/build.gradle | 4 +- .../session/content/ThumbnailExtractor.kt | 34 +- multipicker/build.gradle | 4 +- .../im/vector/lib/multipicker/AudioPicker.kt | 2 +- .../im/vector/lib/multipicker/VideoPicker.kt | 8 +- vector/build.gradle | 4 +- .../app/core/platform/VectorBaseActivity.kt | 22 +- .../preview/AttachmentsPreviewFragment.kt | 7 +- .../app/features/call/VectorCallActivity.kt | 41 +- .../crypto/recover/BootstrapBottomSheet.kt | 8 +- .../app/features/login/LoginWebFragment.kt | 9 +- .../app/features/popup/PopupAlertManager.kt | 9 +- .../app/features/rageshake/BugReporter.kt | 492 +++++++++--------- .../uploads/media/RoomUploadsMediaFragment.kt | 8 +- .../features/webview/VectorWebViewActivity.kt | 4 +- .../features/widgets/webview/WidgetWebView.kt | 3 +- 21 files changed, 424 insertions(+), 322 deletions(-) create mode 100644 attachment-viewer/src/main/res/values/colors.xml diff --git a/CHANGES.md b/CHANGES.md index 373c3aa985..f02c05672d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,10 +15,10 @@ Translations 🗣: - SDK API changes ⚠️: - - + - Increase targetSdkVersion to 30 (#2600) Build 🧱: - - + - Compile with Android SDK 30 (Android 11) Test: - 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 ae095be41a..1d09e1ef0e 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 @@ -24,9 +24,12 @@ 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 @@ -89,14 +92,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) @@ -132,6 +128,25 @@ 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 (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + window.setDecorFitsSystemWindows(false) // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE // New API instead of SYSTEM_UI_FLAG_IMMERSIVE + window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) // New API instead of FLAG_TRANSLUCENT_STATUS + window.navigationBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) // new API instead of FLAG_TRANSLUCENT_NAVIGATION + } 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) @@ -311,28 +326,42 @@ 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 (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + window.setDecorFitsSystemWindows(false) // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars()) // new API instead of SYSTEM_UI_FLAG_HIDE_NAVIGATION + window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE // New API instead of SYSTEM_UI_FLAG_IMMERSIVE + window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) // New API instead of FLAG_TRANSLUCENT_STATUS + window.navigationBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) // new API instead of FLAG_TRANSLUCENT_NAVIGATION + } 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 (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + window.setDecorFitsSystemWindows(false) // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + } 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/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 d72e5bda41..ad177db95c 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 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/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/vector/build.gradle b/vector/build.gradle index f6ba5d6e27..d75bf6c1fe 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -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. 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..3ea995c418 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 @@ -23,6 +23,7 @@ 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 +34,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 +412,21 @@ 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 (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + window.setDecorFitsSystemWindows(false) // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE // New API instead of SYSTEM_UI_FLAG_IMMERSIVE + window.statusBarColor = ContextCompat.getColor(this, im.vector.lib.attachmentviewer.R.color.half_transparent_status_bar) // New API instead of FLAG_TRANSLUCENT_STATUS + window.navigationBarColor = ContextCompat.getColor(this, im.vector.lib.attachmentviewer.R.color.half_transparent_status_bar) // new API instead of FLAG_TRANSLUCENT_NAVIGATION + } 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/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt index 407b51666b..147492fc46 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 @@ -153,8 +153,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 (android.os.Build.VERSION.SDK_INT >= android.os.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/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index 41bf7bbeaf..ef96bc810a 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,43 @@ 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) { + window.setDecorFitsSystemWindows(false) // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars()) // new API instead of SYSTEM_UI_FLAG_HIDE_NAVIGATION + window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE // New API instead of SYSTEM_UI_FLAG_IMMERSIVE + window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) // New API instead of FLAG_TRANSLUCENT_STATUS + window.navigationBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) // new API instead of FLAG_TRANSLUCENT_NAVIGATION + } 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) { + window.setDecorFitsSystemWindows(false) // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + } 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/crypto/recover/BootstrapBottomSheet.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt index f1ea50c9bf..149bd629e1 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 @@ -17,6 +17,7 @@ package im.vector.app.features.crypto.recover import android.app.Dialog +import android.os.Build import android.os.Bundle import android.os.Parcelable import android.view.KeyEvent @@ -102,7 +103,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 } 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/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..fa037a5435 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()) + private val LOGCAT_CMD_ERROR = arrayOf("logcat", // /< Run 'logcat' command "-d", // /< Dump the log rather than continue outputting it "-v", // formatting @@ -160,286 +166,287 @@ 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? { - var bugDescription = theBugDescription - var serverError: String? = null - val crashCallStack = getCrashDescription(context) + coroutineScope.executeAsyncTask( + onPreExecute = { + // NOOP + }, + doInBackground = { publishProgress: suspend (progress: Int) -> Unit -> + var bugDescription = theBugDescription + var serverError: String? = null + val crashCallStack = getCrashDescription(context) - if (null != crashCallStack) { - bugDescription += "\n\n\n\n--------------------------------- crash call stack ---------------------------------\n" - bugDescription += crashCallStack - } - - val gzippedFiles = ArrayList() - - if (withDevicesLogs) { - val files = vectorFileLogger.getLogFiles() - files.mapNotNullTo(gzippedFiles) { f -> - if (!mIsCancelled) { - compressFile(f) - } else { - null - } + if (null != crashCallStack) { + bugDescription += "\n\n\n\n--------------------------------- crash call stack ---------------------------------\n" + bugDescription += crashCallStack } - } - if (!mIsCancelled && (withCrashLogs || withDevicesLogs)) { - val gzippedLogcat = saveLogCat(context, false) + val gzippedFiles = ArrayList() - if (null != gzippedLogcat) { - if (gzippedFiles.size == 0) { - gzippedFiles.add(gzippedLogcat) - } else { - gzippedFiles.add(0, gzippedLogcat) + if (withDevicesLogs) { + val files = vectorFileLogger.getLogFiles() + files.mapNotNullTo(gzippedFiles) { f -> + if (!mIsCancelled) { + compressFile(f) + } else { + null + } } } - val crashDescription = getCrashFile(context) - if (crashDescription.exists()) { - val compressedCrashDescription = compressFile(crashDescription) + if (!mIsCancelled && (withCrashLogs || withDevicesLogs)) { + val gzippedLogcat = saveLogCat(context, false) - if (null != compressedCrashDescription) { + if (null != gzippedLogcat) { if (gzippedFiles.size == 0) { - gzippedFiles.add(compressedCrashDescription) + gzippedFiles.add(gzippedLogcat) } else { - gzippedFiles.add(0, compressedCrashDescription) + gzippedFiles.add(0, gzippedLogcat) + } + } + + val crashDescription = getCrashFile(context) + if (crashDescription.exists()) { + val compressedCrashDescription = compressFile(crashDescription) + + if (null != compressedCrashDescription) { + if (gzippedFiles.size == 0) { + gzippedFiles.add(compressedCrashDescription) + } else { + gzippedFiles.add(0, compressedCrashDescription) + } } } } - } - activeSessionHolder.getSafeActiveSession() - ?.takeIf { !mIsCancelled && withKeyRequestHistory } - ?.cryptoService() - ?.getGossipingEvents() - ?.let { GossipingEventsSerializer().serialize(it) } - ?.toByteArray() - ?.let { rawByteArray -> - File(context.cacheDir.absolutePath, KEY_REQUESTS_FILENAME) - .also { - it.outputStream() - .use { os -> os.write(rawByteArray) } - } - } - ?.let { compressFile(it) } - ?.let { gzippedFiles.add(it) } - - var deviceId = "undefined" - var userId = "undefined" - var olmVersion = "undefined" - - activeSessionHolder.getSafeActiveSession()?.let { session -> - userId = session.myUserId - deviceId = session.sessionParams.deviceId ?: "undefined" - olmVersion = session.cryptoService().getCryptoVersion(context, true) - } - - if (!mIsCancelled) { - val text = "[Element] " + - if (forSuggestion) { - "[Suggestion] " - } else { - "" - } + - bugDescription - - // build the multi part request - val builder = BugReporterMultipartBody.Builder() - .addFormDataPart("text", text) - .addFormDataPart("app", "riot-android") - .addFormDataPart("user_agent", Matrix.getInstance(context).getUserAgent()) - .addFormDataPart("user_id", userId) - .addFormDataPart("device_id", deviceId) - .addFormDataPart("version", versionProvider.getVersion(longFormat = true, useBuildNumber = false)) - .addFormDataPart("branch_name", context.getString(R.string.git_branch_name)) - .addFormDataPart("matrix_sdk_version", Matrix.getSdkVersion()) - .addFormDataPart("olm_version", olmVersion) - .addFormDataPart("device", Build.MODEL.trim()) - .addFormDataPart("verbose_log", vectorPreferences.labAllowedExtendedLogging().toOnOff()) - .addFormDataPart("multi_window", inMultiWindowMode.toOnOff()) - .addFormDataPart("os", Build.VERSION.RELEASE + " (API " + Build.VERSION.SDK_INT + ") " - + Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME) - .addFormDataPart("locale", Locale.getDefault().toString()) - .addFormDataPart("app_language", VectorLocale.applicationLocale.toString()) - .addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString()) - .addFormDataPart("theme", ThemeUtils.getApplicationTheme(context)) - - val buildNumber = context.getString(R.string.build_number) - if (buildNumber.isNotEmpty() && buildNumber != "0") { - builder.addFormDataPart("build_number", buildNumber) - } - - // add the gzipped files - for (file in gzippedFiles) { - builder.addFormDataPart("compressed-log", file.name, file.asRequestBody(MimeTypes.OctetStream.toMediaTypeOrNull())) - } - - mBugReportFiles.addAll(gzippedFiles) - - if (withScreenshot) { - val bitmap = screenshot - - if (null != bitmap) { - val logCatScreenshotFile = File(context.cacheDir.absolutePath, LOG_CAT_SCREENSHOT_FILENAME) - - if (logCatScreenshotFile.exists()) { - logCatScreenshotFile.delete() + activeSessionHolder.getSafeActiveSession() + ?.takeIf { !mIsCancelled && withKeyRequestHistory } + ?.cryptoService() + ?.getGossipingEvents() + ?.let { GossipingEventsSerializer().serialize(it) } + ?.toByteArray() + ?.let { rawByteArray -> + File(context.cacheDir.absolutePath, KEY_REQUESTS_FILENAME) + .also { + it.outputStream() + .use { os -> os.write(rawByteArray) } + } } + ?.let { compressFile(it) } + ?.let { gzippedFiles.add(it) } - try { - logCatScreenshotFile.outputStream().use { - bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) + var deviceId = "undefined" + var userId = "undefined" + var olmVersion = "undefined" + + activeSessionHolder.getSafeActiveSession()?.let { session -> + userId = session.myUserId + deviceId = session.sessionParams.deviceId ?: "undefined" + olmVersion = session.cryptoService().getCryptoVersion(context, true) + } + + if (!mIsCancelled) { + val text = "[Element] " + + if (forSuggestion) { + "[Suggestion] " + } else { + "" + } + + bugDescription + + // build the multi part request + val builder = BugReporterMultipartBody.Builder() + .addFormDataPart("text", text) + .addFormDataPart("app", "riot-android") + .addFormDataPart("user_agent", Matrix.getInstance(context).getUserAgent()) + .addFormDataPart("user_id", userId) + .addFormDataPart("device_id", deviceId) + .addFormDataPart("version", versionProvider.getVersion(longFormat = true, useBuildNumber = false)) + .addFormDataPart("branch_name", context.getString(R.string.git_branch_name)) + .addFormDataPart("matrix_sdk_version", Matrix.getSdkVersion()) + .addFormDataPart("olm_version", olmVersion) + .addFormDataPart("device", Build.MODEL.trim()) + .addFormDataPart("verbose_log", vectorPreferences.labAllowedExtendedLogging().toOnOff()) + .addFormDataPart("multi_window", inMultiWindowMode.toOnOff()) + .addFormDataPart("os", Build.VERSION.RELEASE + " (API " + Build.VERSION.SDK_INT + ") " + + Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME) + .addFormDataPart("locale", Locale.getDefault().toString()) + .addFormDataPart("app_language", VectorLocale.applicationLocale.toString()) + .addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString()) + .addFormDataPart("theme", ThemeUtils.getApplicationTheme(context)) + + val buildNumber = context.getString(R.string.build_number) + if (buildNumber.isNotEmpty() && buildNumber != "0") { + builder.addFormDataPart("build_number", buildNumber) + } + + // add the gzipped files + for (file in gzippedFiles) { + builder.addFormDataPart("compressed-log", file.name, file.asRequestBody(MimeTypes.OctetStream.toMediaTypeOrNull())) + } + + mBugReportFiles.addAll(gzippedFiles) + + if (withScreenshot) { + val bitmap = screenshot + + if (null != bitmap) { + val logCatScreenshotFile = File(context.cacheDir.absolutePath, LOG_CAT_SCREENSHOT_FILENAME) + + if (logCatScreenshotFile.exists()) { + logCatScreenshotFile.delete() } - builder.addFormDataPart("file", - logCatScreenshotFile.name, logCatScreenshotFile.asRequestBody(MimeTypes.OctetStream.toMediaTypeOrNull())) - } catch (e: Exception) { - Timber.e(e, "## sendBugReport() : fail to write screenshot$e") + try { + logCatScreenshotFile.outputStream().use { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) + } + + builder.addFormDataPart("file", + logCatScreenshotFile.name, logCatScreenshotFile.asRequestBody(MimeTypes.OctetStream.toMediaTypeOrNull())) + } catch (e: Exception) { + Timber.e(e, "## sendBugReport() : fail to write screenshot$e") + } } } - } - screenshot = null + screenshot = null - // add some github labels - builder.addFormDataPart("label", BuildConfig.VERSION_NAME) - builder.addFormDataPart("label", BuildConfig.FLAVOR_DESCRIPTION) - builder.addFormDataPart("label", context.getString(R.string.git_branch_name)) + // add some github labels + builder.addFormDataPart("label", BuildConfig.VERSION_NAME) + builder.addFormDataPart("label", BuildConfig.FLAVOR_DESCRIPTION) + builder.addFormDataPart("label", context.getString(R.string.git_branch_name)) - // Special for RiotX - builder.addFormDataPart("label", "[Element]") + // Special for RiotX + builder.addFormDataPart("label", "[Element]") - // Suggestion - if (forSuggestion) { - builder.addFormDataPart("label", "[Suggestion]") - } + // Suggestion + if (forSuggestion) { + builder.addFormDataPart("label", "[Suggestion]") + } - if (getCrashFile(context).exists()) { - builder.addFormDataPart("label", "crash") - deleteCrashFile(context) - } + if (getCrashFile(context).exists()) { + builder.addFormDataPart("label", "crash") + deleteCrashFile(context) + } - val requestBody = builder.build() + val requestBody = builder.build() - // add a progress listener - requestBody.setWriteListener { totalWritten, contentLength -> - val percentage = if (-1L != contentLength) { - if (totalWritten > contentLength) { - 100 + // add a progress listener + requestBody.setWriteListener { totalWritten, contentLength -> + val percentage = if (-1L != contentLength) { + if (totalWritten > contentLength) { + 100 + } else { + (totalWritten * 100 / contentLength).toInt() + } } else { - (totalWritten * 100 / contentLength).toInt() + 0 } - } else { - 0 + + if (mIsCancelled && null != mBugReportCall) { + mBugReportCall!!.cancel() + } + + Timber.v("## onWrite() : $percentage%") + suspend { publishProgress(percentage) } } - if (mIsCancelled && null != mBugReportCall) { - mBugReportCall!!.cancel() + // build the request + val request = Request.Builder() + .url(context.getString(R.string.bug_report_url)) + .post(requestBody) + .build() + + var responseCode = HttpURLConnection.HTTP_INTERNAL_ERROR + var response: Response? = null + var errorMessage: String? = null + + // trigger the request + try { + mBugReportCall = mOkHttpClient.newCall(request) + response = mBugReportCall!!.execute() + responseCode = response.code + } catch (e: Exception) { + Timber.e(e, "response") + errorMessage = e.localizedMessage } - Timber.v("## onWrite() : $percentage%") - publishProgress(percentage) - } + // if the upload failed, try to retrieve the reason + if (responseCode != HttpURLConnection.HTTP_OK) { + if (null != errorMessage) { + serverError = "Failed with error $errorMessage" + } else if (null == response || null == response.body) { + serverError = "Failed with error $responseCode" + } else { + try { + val inputStream = response.body!!.byteStream() - // build the request - val request = Request.Builder() - .url(context.getString(R.string.bug_report_url)) - .post(requestBody) - .build() - - var responseCode = HttpURLConnection.HTTP_INTERNAL_ERROR - var response: Response? = null - var errorMessage: String? = null - - // trigger the request - try { - mBugReportCall = mOkHttpClient.newCall(request) - response = mBugReportCall!!.execute() - responseCode = response.code - } catch (e: Exception) { - Timber.e(e, "response") - errorMessage = e.localizedMessage - } - - // if the upload failed, try to retrieve the reason - if (responseCode != HttpURLConnection.HTTP_OK) { - if (null != errorMessage) { - serverError = "Failed with error $errorMessage" - } else if (null == response || null == response.body) { - serverError = "Failed with error $responseCode" - } else { - try { - val inputStream = response.body!!.byteStream() - - serverError = inputStream.use { - buildString { - var ch = it.read() - while (ch != -1) { - append(ch.toChar()) - ch = it.read() + serverError = inputStream.use { + buildString { + var ch = it.read() + while (ch != -1) { + append(ch.toChar()) + ch = it.read() + } } } - } - // check if the error message - try { - val responseJSON = JSONObject(serverError) - serverError = responseJSON.getString("error") - } catch (e: JSONException) { - Timber.e(e, "doInBackground ; Json conversion failed") - } + // check if the error message + try { + val responseJSON = JSONObject(serverError) + serverError = responseJSON.getString("error") + } catch (e: JSONException) { + Timber.e(e, "doInBackground ; Json conversion failed") + } - // should never happen - if (null == serverError) { - serverError = "Failed with error $responseCode" + // should never happen + if (null == serverError) { + serverError = "Failed with error $responseCode" + } + } catch (e: Exception) { + Timber.e(e, "## sendBugReport() : failed to parse error") } - } catch (e: Exception) { - Timber.e(e, "## sendBugReport() : failed to parse error") } } } - } - 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?) { - mBugReportCall = null - - // delete when the bug report has been successfully sent - for (file in mBugReportFiles) { - file.delete() - } - - if (null != listener) { - try { - if (mIsCancelled) { - listener.onUploadCancelled() - } else if (null == reason) { - listener.onUploadSucceed() - } else { - listener.onUploadFailed(reason) + serverError + }, + onProgressUpdate = { progress -> + if (null != listener) { + try { + listener.onProgress(progress) + } catch (e: Exception) { + Timber.e(e, "## onProgress() : failed") + } + } + }, + onPostExecute = { reason: String? -> + mBugReportCall = null + + // delete when the bug report has been successfully sent + for (file in mBugReportFiles) { + file.delete() + } + + if (null != listener) { + try { + if (mIsCancelled) { + listener.onUploadCancelled() + } else if (null == reason) { + listener.onUploadSucceed() + } else { + listener.onUploadFailed(reason) + } + } catch (e: Exception) { + Timber.e(e, "## onPostExecute() : failed") } - } catch (e: Exception) { - Timber.e(e, "## onPostExecute() : failed") } } - } - }.execute() + ) } /** @@ -696,4 +703,21 @@ class BugReporter @Inject constructor( return null } + + fun CoroutineScope.executeAsyncTask( + onPreExecute: () -> Unit, + doInBackground: suspend (suspend (P) -> Unit) -> R, + onPostExecute: (R) -> Unit, + onProgressUpdate: (P) -> Unit + ) = launch { + onPreExecute() + + val result = withContext(Dispatchers.IO) { + doInBackground { + withContext(Dispatchers.Main) { onProgressUpdate(it) } + } + } + + onPostExecute(result) + } } 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/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/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) From fa311f4ce28ba9e0a944d3dce02ec5910338eafd Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 5 Jan 2021 14:44:02 +0300 Subject: [PATCH 02/51] Fix bug reporter progress. --- .../app/features/rageshake/BugReporter.kt | 516 +++++++++--------- 1 file changed, 246 insertions(+), 270 deletions(-) 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 fa037a5435..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 @@ -102,7 +102,7 @@ class BugReporter @Inject constructor( var screenshot: Bitmap? = null private set - private val coroutineScope = CoroutineScope(SupervisorJob()) + 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 @@ -169,284 +169,277 @@ class BugReporter @Inject constructor( // enumerate files to delete val mBugReportFiles: MutableList = ArrayList() - coroutineScope.executeAsyncTask( - onPreExecute = { - // NOOP - }, - doInBackground = { publishProgress: suspend (progress: Int) -> Unit -> - var bugDescription = theBugDescription - var serverError: String? = null - val crashCallStack = getCrashDescription(context) + coroutineScope.launch { + var serverError: String? = null + withContext(Dispatchers.IO) { + var bugDescription = theBugDescription + val crashCallStack = getCrashDescription(context) - if (null != crashCallStack) { - bugDescription += "\n\n\n\n--------------------------------- crash call stack ---------------------------------\n" - bugDescription += crashCallStack + if (null != crashCallStack) { + bugDescription += "\n\n\n\n--------------------------------- crash call stack ---------------------------------\n" + bugDescription += crashCallStack + } + + val gzippedFiles = ArrayList() + + if (withDevicesLogs) { + val files = vectorFileLogger.getLogFiles() + files.mapNotNullTo(gzippedFiles) { f -> + if (!mIsCancelled) { + compressFile(f) + } else { + null + } } + } - val gzippedFiles = ArrayList() + if (!mIsCancelled && (withCrashLogs || withDevicesLogs)) { + val gzippedLogcat = saveLogCat(context, false) - if (withDevicesLogs) { - val files = vectorFileLogger.getLogFiles() - files.mapNotNullTo(gzippedFiles) { f -> - if (!mIsCancelled) { - compressFile(f) - } else { - null - } + if (null != gzippedLogcat) { + if (gzippedFiles.size == 0) { + gzippedFiles.add(gzippedLogcat) + } else { + gzippedFiles.add(0, gzippedLogcat) } } - if (!mIsCancelled && (withCrashLogs || withDevicesLogs)) { - val gzippedLogcat = saveLogCat(context, false) + val crashDescription = getCrashFile(context) + if (crashDescription.exists()) { + val compressedCrashDescription = compressFile(crashDescription) - if (null != gzippedLogcat) { + if (null != compressedCrashDescription) { if (gzippedFiles.size == 0) { - gzippedFiles.add(gzippedLogcat) + gzippedFiles.add(compressedCrashDescription) } else { - gzippedFiles.add(0, gzippedLogcat) - } - } - - val crashDescription = getCrashFile(context) - if (crashDescription.exists()) { - val compressedCrashDescription = compressFile(crashDescription) - - if (null != compressedCrashDescription) { - if (gzippedFiles.size == 0) { - gzippedFiles.add(compressedCrashDescription) - } else { - gzippedFiles.add(0, compressedCrashDescription) - } + gzippedFiles.add(0, compressedCrashDescription) } } } + } - activeSessionHolder.getSafeActiveSession() - ?.takeIf { !mIsCancelled && withKeyRequestHistory } - ?.cryptoService() - ?.getGossipingEvents() - ?.let { GossipingEventsSerializer().serialize(it) } - ?.toByteArray() - ?.let { rawByteArray -> - File(context.cacheDir.absolutePath, KEY_REQUESTS_FILENAME) - .also { - it.outputStream() - .use { os -> os.write(rawByteArray) } - } - } - ?.let { compressFile(it) } - ?.let { gzippedFiles.add(it) } - - var deviceId = "undefined" - var userId = "undefined" - var olmVersion = "undefined" - - activeSessionHolder.getSafeActiveSession()?.let { session -> - userId = session.myUserId - deviceId = session.sessionParams.deviceId ?: "undefined" - olmVersion = session.cryptoService().getCryptoVersion(context, true) - } - - if (!mIsCancelled) { - val text = "[Element] " + - if (forSuggestion) { - "[Suggestion] " - } else { - "" - } + - bugDescription - - // build the multi part request - val builder = BugReporterMultipartBody.Builder() - .addFormDataPart("text", text) - .addFormDataPart("app", "riot-android") - .addFormDataPart("user_agent", Matrix.getInstance(context).getUserAgent()) - .addFormDataPart("user_id", userId) - .addFormDataPart("device_id", deviceId) - .addFormDataPart("version", versionProvider.getVersion(longFormat = true, useBuildNumber = false)) - .addFormDataPart("branch_name", context.getString(R.string.git_branch_name)) - .addFormDataPart("matrix_sdk_version", Matrix.getSdkVersion()) - .addFormDataPart("olm_version", olmVersion) - .addFormDataPart("device", Build.MODEL.trim()) - .addFormDataPart("verbose_log", vectorPreferences.labAllowedExtendedLogging().toOnOff()) - .addFormDataPart("multi_window", inMultiWindowMode.toOnOff()) - .addFormDataPart("os", Build.VERSION.RELEASE + " (API " + Build.VERSION.SDK_INT + ") " - + Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME) - .addFormDataPart("locale", Locale.getDefault().toString()) - .addFormDataPart("app_language", VectorLocale.applicationLocale.toString()) - .addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString()) - .addFormDataPart("theme", ThemeUtils.getApplicationTheme(context)) - - val buildNumber = context.getString(R.string.build_number) - if (buildNumber.isNotEmpty() && buildNumber != "0") { - builder.addFormDataPart("build_number", buildNumber) - } - - // add the gzipped files - for (file in gzippedFiles) { - builder.addFormDataPart("compressed-log", file.name, file.asRequestBody(MimeTypes.OctetStream.toMediaTypeOrNull())) - } - - mBugReportFiles.addAll(gzippedFiles) - - if (withScreenshot) { - val bitmap = screenshot - - if (null != bitmap) { - val logCatScreenshotFile = File(context.cacheDir.absolutePath, LOG_CAT_SCREENSHOT_FILENAME) - - if (logCatScreenshotFile.exists()) { - logCatScreenshotFile.delete() - } - - try { - logCatScreenshotFile.outputStream().use { - bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) + activeSessionHolder.getSafeActiveSession() + ?.takeIf { !mIsCancelled && withKeyRequestHistory } + ?.cryptoService() + ?.getGossipingEvents() + ?.let { GossipingEventsSerializer().serialize(it) } + ?.toByteArray() + ?.let { rawByteArray -> + File(context.cacheDir.absolutePath, KEY_REQUESTS_FILENAME) + .also { + it.outputStream() + .use { os -> os.write(rawByteArray) } } - - builder.addFormDataPart("file", - logCatScreenshotFile.name, logCatScreenshotFile.asRequestBody(MimeTypes.OctetStream.toMediaTypeOrNull())) - } catch (e: Exception) { - Timber.e(e, "## sendBugReport() : fail to write screenshot$e") - } - } } + ?.let { compressFile(it) } + ?.let { gzippedFiles.add(it) } - screenshot = null + var deviceId = "undefined" + var userId = "undefined" + var olmVersion = "undefined" - // add some github labels - builder.addFormDataPart("label", BuildConfig.VERSION_NAME) - builder.addFormDataPart("label", BuildConfig.FLAVOR_DESCRIPTION) - builder.addFormDataPart("label", context.getString(R.string.git_branch_name)) + activeSessionHolder.getSafeActiveSession()?.let { session -> + userId = session.myUserId + deviceId = session.sessionParams.deviceId ?: "undefined" + olmVersion = session.cryptoService().getCryptoVersion(context, true) + } - // Special for RiotX - builder.addFormDataPart("label", "[Element]") - - // Suggestion - if (forSuggestion) { - builder.addFormDataPart("label", "[Suggestion]") - } - - if (getCrashFile(context).exists()) { - builder.addFormDataPart("label", "crash") - deleteCrashFile(context) - } - - val requestBody = builder.build() - - // add a progress listener - requestBody.setWriteListener { totalWritten, contentLength -> - val percentage = if (-1L != contentLength) { - if (totalWritten > contentLength) { - 100 - } else { - (totalWritten * 100 / contentLength).toInt() - } + if (!mIsCancelled) { + val text = "[Element] " + + if (forSuggestion) { + "[Suggestion] " } else { - 0 + "" + } + + bugDescription + + // build the multi part request + val builder = BugReporterMultipartBody.Builder() + .addFormDataPart("text", text) + .addFormDataPart("app", "riot-android") + .addFormDataPart("user_agent", Matrix.getInstance(context).getUserAgent()) + .addFormDataPart("user_id", userId) + .addFormDataPart("device_id", deviceId) + .addFormDataPart("version", versionProvider.getVersion(longFormat = true, useBuildNumber = false)) + .addFormDataPart("branch_name", context.getString(R.string.git_branch_name)) + .addFormDataPart("matrix_sdk_version", Matrix.getSdkVersion()) + .addFormDataPart("olm_version", olmVersion) + .addFormDataPart("device", Build.MODEL.trim()) + .addFormDataPart("verbose_log", vectorPreferences.labAllowedExtendedLogging().toOnOff()) + .addFormDataPart("multi_window", inMultiWindowMode.toOnOff()) + .addFormDataPart("os", Build.VERSION.RELEASE + " (API " + Build.VERSION.SDK_INT + ") " + + Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME) + .addFormDataPart("locale", Locale.getDefault().toString()) + .addFormDataPart("app_language", VectorLocale.applicationLocale.toString()) + .addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString()) + .addFormDataPart("theme", ThemeUtils.getApplicationTheme(context)) + + val buildNumber = context.getString(R.string.build_number) + if (buildNumber.isNotEmpty() && buildNumber != "0") { + builder.addFormDataPart("build_number", buildNumber) + } + + // add the gzipped files + for (file in gzippedFiles) { + builder.addFormDataPart("compressed-log", file.name, file.asRequestBody(MimeTypes.OctetStream.toMediaTypeOrNull())) + } + + mBugReportFiles.addAll(gzippedFiles) + + if (withScreenshot) { + val bitmap = screenshot + + if (null != bitmap) { + val logCatScreenshotFile = File(context.cacheDir.absolutePath, LOG_CAT_SCREENSHOT_FILENAME) + + if (logCatScreenshotFile.exists()) { + logCatScreenshotFile.delete() } - if (mIsCancelled && null != mBugReportCall) { - mBugReportCall!!.cancel() - } + try { + logCatScreenshotFile.outputStream().use { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) + } - Timber.v("## onWrite() : $percentage%") - suspend { publishProgress(percentage) } + builder.addFormDataPart("file", + logCatScreenshotFile.name, logCatScreenshotFile.asRequestBody(MimeTypes.OctetStream.toMediaTypeOrNull())) + } catch (e: Exception) { + Timber.e(e, "## sendBugReport() : fail to write screenshot$e") + } + } + } + + screenshot = null + + // add some github labels + builder.addFormDataPart("label", BuildConfig.VERSION_NAME) + builder.addFormDataPart("label", BuildConfig.FLAVOR_DESCRIPTION) + builder.addFormDataPart("label", context.getString(R.string.git_branch_name)) + + // Special for RiotX + builder.addFormDataPart("label", "[Element]") + + // Suggestion + if (forSuggestion) { + builder.addFormDataPart("label", "[Suggestion]") + } + + if (getCrashFile(context).exists()) { + builder.addFormDataPart("label", "crash") + deleteCrashFile(context) + } + + val requestBody = builder.build() + + // add a progress listener + requestBody.setWriteListener { totalWritten, contentLength -> + val percentage = if (-1L != contentLength) { + if (totalWritten > contentLength) { + 100 + } else { + (totalWritten * 100 / contentLength).toInt() + } + } else { + 0 } - // build the request - val request = Request.Builder() - .url(context.getString(R.string.bug_report_url)) - .post(requestBody) - .build() + if (mIsCancelled && null != mBugReportCall) { + mBugReportCall!!.cancel() + } - var responseCode = HttpURLConnection.HTTP_INTERNAL_ERROR - var response: Response? = null - var errorMessage: String? = null - - // trigger the request + Timber.v("## onWrite() : $percentage%") try { - mBugReportCall = mOkHttpClient.newCall(request) - response = mBugReportCall!!.execute() - responseCode = response.code - } catch (e: Exception) { - Timber.e(e, "response") - errorMessage = e.localizedMessage - } - - // if the upload failed, try to retrieve the reason - if (responseCode != HttpURLConnection.HTTP_OK) { - if (null != errorMessage) { - serverError = "Failed with error $errorMessage" - } else if (null == response || null == response.body) { - serverError = "Failed with error $responseCode" - } else { - try { - val inputStream = response.body!!.byteStream() - - serverError = inputStream.use { - buildString { - var ch = it.read() - while (ch != -1) { - append(ch.toChar()) - ch = it.read() - } - } - } - - // check if the error message - try { - val responseJSON = JSONObject(serverError) - serverError = responseJSON.getString("error") - } catch (e: JSONException) { - Timber.e(e, "doInBackground ; Json conversion failed") - } - - // should never happen - if (null == serverError) { - serverError = "Failed with error $responseCode" - } - } catch (e: Exception) { - Timber.e(e, "## sendBugReport() : failed to parse error") - } - } - } - } - - serverError - }, - onProgressUpdate = { progress -> - if (null != listener) { - try { - listener.onProgress(progress) + listener?.onProgress(percentage) } catch (e: Exception) { Timber.e(e, "## onProgress() : failed") } } - }, - onPostExecute = { reason: String? -> - mBugReportCall = null - // delete when the bug report has been successfully sent - for (file in mBugReportFiles) { - file.delete() + // build the request + val request = Request.Builder() + .url(context.getString(R.string.bug_report_url)) + .post(requestBody) + .build() + + var responseCode = HttpURLConnection.HTTP_INTERNAL_ERROR + var response: Response? = null + var errorMessage: String? = null + + // trigger the request + try { + mBugReportCall = mOkHttpClient.newCall(request) + response = mBugReportCall!!.execute() + responseCode = response.code + } catch (e: Exception) { + Timber.e(e, "response") + errorMessage = e.localizedMessage } - if (null != listener) { - try { - if (mIsCancelled) { - listener.onUploadCancelled() - } else if (null == reason) { - listener.onUploadSucceed() - } else { - listener.onUploadFailed(reason) + // if the upload failed, try to retrieve the reason + if (responseCode != HttpURLConnection.HTTP_OK) { + if (null != errorMessage) { + serverError = "Failed with error $errorMessage" + } else if (null == response || null == response.body) { + serverError = "Failed with error $responseCode" + } else { + try { + val inputStream = response.body!!.byteStream() + + serverError = inputStream.use { + buildString { + var ch = it.read() + while (ch != -1) { + append(ch.toChar()) + ch = it.read() + } + } + } + + // check if the error message + serverError?.let { + try { + val responseJSON = JSONObject(it) + serverError = responseJSON.getString("error") + } catch (e: JSONException) { + Timber.e(e, "doInBackground ; Json conversion failed") + } + } + + // should never happen + if (null == serverError) { + serverError = "Failed with error $responseCode" + } + } catch (e: Exception) { + Timber.e(e, "## sendBugReport() : failed to parse error") } - } catch (e: Exception) { - Timber.e(e, "## onPostExecute() : failed") } } } - ) + } + + withContext(Dispatchers.Main) { + mBugReportCall = null + + // delete when the bug report has been successfully sent + for (file in mBugReportFiles) { + file.delete() + } + + if (null != listener) { + try { + if (mIsCancelled) { + listener.onUploadCancelled() + } else if (null == serverError) { + listener.onUploadSucceed() + } else { + listener.onUploadFailed(serverError) + } + } catch (e: Exception) { + Timber.e(e, "## onPostExecute() : failed") + } + } + } + } } /** @@ -464,9 +457,9 @@ class BugReporter @Inject constructor( activity.startActivity(intent) } - // ============================================================================================================== - // crash report management - // ============================================================================================================== +// ============================================================================================================== +// crash report management +// ============================================================================================================== /** * Provides the crash file @@ -536,9 +529,9 @@ class BugReporter @Inject constructor( return null } - // ============================================================================================================== - // Screenshot management - // ============================================================================================================== +// ============================================================================================================== +// Screenshot management +// ============================================================================================================== /** * Take a screenshot of the display. @@ -605,9 +598,9 @@ class BugReporter @Inject constructor( } } - // ============================================================================================================== - // Logcat management - // ============================================================================================================== +// ============================================================================================================== +// Logcat management +// ============================================================================================================== /** * Save the logcat @@ -667,9 +660,9 @@ class BugReporter @Inject constructor( } } - // ============================================================================================================== - // File compression management - // ============================================================================================================== +// ============================================================================================================== +// File compression management +// ============================================================================================================== /** * GZip a file @@ -703,21 +696,4 @@ class BugReporter @Inject constructor( return null } - - fun CoroutineScope.executeAsyncTask( - onPreExecute: () -> Unit, - doInBackground: suspend (suspend (P) -> Unit) -> R, - onPostExecute: (R) -> Unit, - onProgressUpdate: (P) -> Unit - ) = launch { - onPreExecute() - - val result = withContext(Dispatchers.IO) { - doInBackground { - withContext(Dispatchers.Main) { onProgressUpdate(it) } - } - } - - onPostExecute(result) - } } From 869eb262f33ee87cdfeffcf128c166d4586e87df Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 6 Jan 2021 12:49:11 +0300 Subject: [PATCH 03/51] Lint fixes. --- .../AttachmentViewerActivity.kt | 37 ++++++++++++------- .../app/core/platform/VectorBaseActivity.kt | 15 +++++--- .../preview/AttachmentsPreviewFragment.kt | 3 +- .../app/features/call/VectorCallActivity.kt | 18 ++++++--- 4 files changed, 48 insertions(+), 25 deletions(-) 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 1d09e1ef0e..88109e7ea0 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,6 +18,7 @@ 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 @@ -132,11 +133,15 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi private fun setDecorViewFullScreen() { // This is important for the dispatchTouchEvent, if not we must correct // the touch coordinates - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { - window.setDecorFitsSystemWindows(false) // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE // New API instead of SYSTEM_UI_FLAG_IMMERSIVE - window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) // New API instead of FLAG_TRANSLUCENT_STATUS - window.navigationBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) // new API instead of FLAG_TRANSLUCENT_NAVIGATION + 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 @@ -332,12 +337,17 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi // 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 - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { - window.setDecorFitsSystemWindows(false) // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars()) // new API instead of SYSTEM_UI_FLAG_HIDE_NAVIGATION - window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE // New API instead of SYSTEM_UI_FLAG_IMMERSIVE - window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) // New API instead of FLAG_TRANSLUCENT_STATUS - window.navigationBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) // new API instead of FLAG_TRANSLUCENT_NAVIGATION + 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 @@ -356,8 +366,9 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi @Suppress("DEPRECATION") private fun showSystemUI() { systemUiVisibility = true - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { - window.setDecorFitsSystemWindows(false) // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + 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 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 3ea995c418..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,6 +19,7 @@ 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 @@ -414,11 +415,15 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScr */ @Suppress("DEPRECATION") private fun setFullScreen() { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { - window.setDecorFitsSystemWindows(false) // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE // New API instead of SYSTEM_UI_FLAG_IMMERSIVE - window.statusBarColor = ContextCompat.getColor(this, im.vector.lib.attachmentviewer.R.color.half_transparent_status_bar) // New API instead of FLAG_TRANSLUCENT_STATUS - window.navigationBarColor = ContextCompat.getColor(this, im.vector.lib.attachmentviewer.R.color.half_transparent_status_bar) // new API instead of FLAG_TRANSLUCENT_NAVIGATION + 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 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 147492fc46..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 @@ -155,7 +156,7 @@ class AttachmentsPreviewFragment @Inject constructor( @Suppress("DEPRECATION") private fun applyInsets() { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + 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 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 ef96bc810a..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 @@ -112,11 +112,16 @@ class VectorCallActivity : VectorBaseActivity(), CallContro // For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE. // Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - window.setDecorFitsSystemWindows(false) // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars()) // new API instead of SYSTEM_UI_FLAG_HIDE_NAVIGATION - window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE // New API instead of SYSTEM_UI_FLAG_IMMERSIVE - window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) // New API instead of FLAG_TRANSLUCENT_STATUS - window.navigationBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) // new API instead of FLAG_TRANSLUCENT_NAVIGATION + // 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 @@ -136,7 +141,8 @@ class VectorCallActivity : VectorBaseActivity(), CallContro private fun showSystemUI() { systemUiVisibility = true if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - window.setDecorFitsSystemWindows(false) // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + // 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 From e948e9d85ad3f4ba84b85bfe82337762c02bec88 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 6 Jan 2021 16:28:45 +0300 Subject: [PATCH 04/51] Lint fixes. --- .../main/java/im/vector/app/features/call/CallAudioManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 36b1a1471aa788dd973cff699e0322a92df353bf Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 13 Jan 2021 14:23:02 +0100 Subject: [PATCH 05/51] Fix Dendrite sync response support --- .../room/timeline/EventContextResponse.kt | 12 ++++++++---- .../session/room/timeline/PaginationResponse.kt | 4 ++-- .../session/room/timeline/TokenChunkEvent.kt | 4 ++-- .../room/timeline/TokenChunkEventPersistor.kt | 16 ++++++++-------- .../session/room/uploads/GetUploadsTask.kt | 2 +- 5 files changed, 21 insertions(+), 17 deletions(-) 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..d76ba35280 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 @@ -24,13 +24,17 @@ import org.matrix.android.sdk.api.session.events.model.Event data class EventContextResponse( @Json(name = "event") val event: Event, @Json(name = "start") override val start: String? = null, - @Json(name = "events_before") val eventsBefore: List = emptyList(), - @Json(name = "events_after") val eventsAfter: List = emptyList(), + @Json(name = "events_before") val eventsBefore: List? = emptyList(), + @Json(name = "events_after") val eventsAfter: List? = emptyList(), @Json(name = "end") override val end: String? = null, - @Json(name = "state") override val stateEvents: List = emptyList() + @Json(name = "state") override val stateEvents: List? = emptyList() ) : TokenChunkEvent { override val events: List by lazy { - eventsAfter.reversed() + listOf(event) + eventsBefore + mutableListOf().apply { + eventsAfter?.let { addAll(it.reversed()) } + add(event) + eventsBefore?.let { addAll(it) } + } } } 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..ff0c7fbbde 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 @@ -24,6 +24,6 @@ import org.matrix.android.sdk.api.session.events.model.Event internal data class PaginationResponse( @Json(name = "start") override val start: String? = null, @Json(name = "end") override val end: String? = null, - @Json(name = "chunk") override val events: List = emptyList(), - @Json(name = "state") override val stateEvents: List = emptyList() + @Json(name = "chunk") override val events: List? = emptyList(), + @Json(name = "state") override val stateEvents: List? = emptyList() ) : TokenChunkEvent 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..50cc50beb2 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 @@ -21,8 +21,8 @@ import org.matrix.android.sdk.api.session.events.model.Event internal interface TokenChunkEvent { val start: String? val end: String? - val events: List - val stateEvents: List + val events: 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..fb636a9314 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 @@ -124,7 +124,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri direction: PaginationDirection): Result { monarchy .awaitTransaction { realm -> - Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction") + Timber.v("Start persisting ${receivedChunk.events?.size} events in $roomId towards $direction") val nextToken: String? val prevToken: String? @@ -149,13 +149,13 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri } ?: ChunkEntity.create(realm, prevToken, nextToken) - if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) { + if (receivedChunk.events.isNullOrEmpty() && !receivedChunk.hasMore()) { handleReachEnd(realm, roomId, direction, currentChunk) } else { handlePagination(realm, roomId, direction, receivedChunk, currentChunk) } } - return if (receivedChunk.events.isEmpty()) { + return if (receivedChunk.events.isNullOrEmpty()) { if (receivedChunk.start != receivedChunk.end) { Result.SHOULD_FETCH_MORE } else { @@ -189,14 +189,14 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri receivedChunk: TokenChunkEvent, currentChunk: ChunkEntity ) { - Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}") + Timber.v("Add ${receivedChunk.events?.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}") val roomMemberContentsByUser = HashMap() val eventList = receivedChunk.events val stateEvents = receivedChunk.stateEvents 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) @@ -204,10 +204,10 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri roomMemberContentsByUser[stateEvent.stateKey] = stateEvent.content.toModel() } } - val eventIds = ArrayList(eventList.size) - for (event in eventList) { + val eventIds = ArrayList(eventList?.size ?: 0) + 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/uploads/GetUploadsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt index 0c0e6a8ed0..9d66474a73 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 @@ -95,7 +95,7 @@ internal class DefaultGetUploadsTask @Inject constructor( nextToken = chunk.end ?: "", hasMore = chunk.hasMore() ) - events = chunk.events + events = chunk.events ?: emptyList() } var uploadEvents = listOf() From f36bab0a7ad72cd38d2d39030b3f72040b195653 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 19 Jan 2021 15:33:35 +0300 Subject: [PATCH 06/51] Limit drawer layout with max width. --- CHANGES.md | 2 +- vector/src/main/res/layout/activity_home.xml | 2 +- vector/src/main/res/values-sw600dp/dimens.xml | 5 +++++ vector/src/main/res/values/dimens.xml | 3 +++ 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 vector/src/main/res/values-sw600dp/dimens.xml diff --git a/CHANGES.md b/CHANGES.md index c9c9c5d035..6a1b5447e4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,7 @@ Improvements 🙌: - Bugfix 🐛: - - + - Sidebar too large in horizontal orientation or tablets (#475) Translations 🗣: - 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/values-sw600dp/dimens.xml b/vector/src/main/res/values-sw600dp/dimens.xml new file mode 100644 index 0000000000..204d663d9c --- /dev/null +++ b/vector/src/main/res/values-sw600dp/dimens.xml @@ -0,0 +1,5 @@ + + + + 400dp + \ No newline at end of file diff --git a/vector/src/main/res/values/dimens.xml b/vector/src/main/res/values/dimens.xml index ccb7ae7726..06ca39950c 100644 --- a/vector/src/main/res/values/dimens.xml +++ b/vector/src/main/res/values/dimens.xml @@ -33,4 +33,7 @@ 280dp + + 320dp + \ No newline at end of file From 4709002429509b22780889822a542c16cf2c6aa2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Jan 2021 15:19:21 +0100 Subject: [PATCH 07/51] Fix clear cache issue: sometimes, after a clear cache, there is still a token, so the init sync service is not started. Migratiing to supend methods fixes the problem, I'm not sure why... --- CHANGES.md | 2 +- .../sdk/api/session/cache/CacheService.kt | 4 +- .../sdk/api/session/signout/SignOutService.kt | 11 +- .../sdk/internal/session/DefaultSession.kt | 4 +- .../session/cache/DefaultCacheService.kt | 13 +-- .../session/signout/DefaultSignOutService.kt | 36 ++----- .../im/vector/app/core/platform/EmptyState.kt | 23 ++++ .../app/core/platform/EmptyViewModel.kt | 26 +++++ .../im/vector/app/features/MainActivity.kt | 102 +++++++++--------- .../app/features/link/LinkHandlerActivity.kt | 40 ++++--- .../signout/soft/SoftLogoutViewModel.kt | 54 +++++----- 11 files changed, 172 insertions(+), 143 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/platform/EmptyState.kt create mode 100644 vector/src/main/java/im/vector/app/core/platform/EmptyViewModel.kt diff --git a/CHANGES.md b/CHANGES.md index c9c9c5d035..cc667656ad 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,7 @@ Improvements 🙌: - Bugfix 🐛: - - + - Fix clear cache issue: sometimes, after a clear cache, there is still a token, so the init sync service is not started. Translations 🗣: - 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/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/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index fa07b16c32..def865328c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -217,13 +217,13 @@ internal class DefaultSession @Inject constructor( } } - override fun clearCache(callback: MatrixCallback) { + override suspend fun clearCache() { stopSync() stopAnyBackgroundSync() uiHandler.post { lifecycleObservers.forEach { it.onClearCache() } } - cacheService.get().clearCache(callback) + cacheService.get().clearCache() workManagerProvider.cancelAllWorks() } 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/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/vector/src/main/java/im/vector/app/core/platform/EmptyState.kt b/vector/src/main/java/im/vector/app/core/platform/EmptyState.kt new file mode 100644 index 0000000000..c58532c6e0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/platform/EmptyState.kt @@ -0,0 +1,23 @@ +/* + * 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.core.platform + +import com.airbnb.mvrx.MvRxState + +data class EmptyState( + val dummy: Int = 0 +) : MvRxState diff --git a/vector/src/main/java/im/vector/app/core/platform/EmptyViewModel.kt b/vector/src/main/java/im/vector/app/core/platform/EmptyViewModel.kt new file mode 100644 index 0000000000..420c58d44a --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/platform/EmptyViewModel.kt @@ -0,0 +1,26 @@ +/* + * 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.core.platform + +/** + * Mainly used to get a viewModelScope + */ +class EmptyViewModel(initialState: EmptyState) : VectorViewModel(initialState) { + override fun handle(action: EmptyAction) { + // N/A + } +} 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..beb9be8b9d 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -22,12 +22,15 @@ import android.os.Bundle import android.os.Parcelable import androidx.appcompat.app.AlertDialog import androidx.lifecycle.Lifecycle +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.viewModel import com.bumptech.glide.Glide import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.startSyncing +import im.vector.app.core.platform.EmptyViewModel import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.deleteAllFiles import im.vector.app.databinding.FragmentLoadingBinding @@ -45,10 +48,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 @@ -82,6 +83,8 @@ class MainActivity : VectorBaseActivity(), UnlockedActiv } } + private val emptyViewModel: EmptyViewModel by viewModel() + override fun getBinding() = FragmentLoadingBinding.inflate(layoutInflater) private lateinit var args: MainActivityArgs @@ -147,38 +150,41 @@ class MainActivity : VectorBaseActivity(), UnlockedActiv } when { args.isAccountDeactivated -> { - // Just do the local cleanup - Timber.w("Account deactivated, start app") - sessionHolder.clearActiveSession() - doLocalCleanup(clearPreferences = true) - startNextActivityAndFinish() + emptyViewModel.viewModelScope.launch { + // Just do the local cleanup + Timber.w("Account deactivated, start app") + sessionHolder.clearActiveSession() + doLocalCleanup(clearPreferences = true) + startNextActivityAndFinish() + } + } + args.clearCredentials -> { + emptyViewModel.viewModelScope.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 -> { + emptyViewModel.viewModelScope.launch { + try { + session.clearCache() + Timber.e("CACHE success") + doLocalCleanup(clearPreferences = false) + session.startSyncing(applicationContext) + startNextActivityAndFinish() + } catch (failure: Throwable) { + Timber.e("CACHE failure") + 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 +193,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/link/LinkHandlerActivity.kt b/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt index 2f1e8c0b1a..b71d0a429e 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,10 +19,13 @@ package im.vector.app.features.link import android.content.Intent import android.net.Uri import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.viewModel import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.platform.EmptyViewModel import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.toast import im.vector.app.databinding.ActivityProgressBinding @@ -30,7 +33,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 @@ -45,6 +48,8 @@ class LinkHandlerActivity : VectorBaseActivity() { @Inject lateinit var errorFormatter: ErrorFormatter @Inject lateinit var permalinkHandler: PermalinkHandler + private val emptyViewModel: EmptyViewModel by viewModel() + override fun injectWith(injector: ScreenComponent) { injector.inject(this) } @@ -139,23 +144,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 { + emptyViewModel.viewModelScope.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/signout/soft/SoftLogoutViewModel.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt index bebaa09063..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 @@ -30,6 +31,7 @@ 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 @@ -174,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 + ) } - ) + } + } } } } @@ -202,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() { From c75eb050dfed69ffda7950ca4a88330e7f4f39a1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Jan 2021 16:32:41 +0100 Subject: [PATCH 08/51] Cleanup and fix test compilation --- .../java/org/matrix/android/sdk/common/CommonTestHelper.kt | 4 +++- .../session/room/timeline/TimelineForwardPaginationTest.kt | 4 ++-- .../org/matrix/android/sdk/internal/session/DefaultSession.kt | 1 - vector/src/main/java/im/vector/app/features/MainActivity.kt | 2 -- .../java/im/vector/app/features/link/LinkHandlerActivity.kt | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) 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/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/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index def865328c..fc5941bc61 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -20,7 +20,6 @@ import androidx.annotation.MainThread import dagger.Lazy import io.realm.RealmConfiguration import okhttp3.OkHttpClient -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.pushrules.PushRuleService 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 beb9be8b9d..f8b8229e70 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -175,12 +175,10 @@ class MainActivity : VectorBaseActivity(), UnlockedActiv emptyViewModel.viewModelScope.launch { try { session.clearCache() - Timber.e("CACHE success") doLocalCleanup(clearPreferences = false) session.startSyncing(applicationContext) startNextActivityAndFinish() } catch (failure: Throwable) { - Timber.e("CACHE failure") displayError(failure) } } 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 b71d0a429e..7183f27980 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 @@ -151,7 +151,7 @@ class LinkHandlerActivity : VectorBaseActivity() { private fun safeSignout(uri: Uri) { val session = sessionHolder.getSafeActiveSession() - if(session == null) { + if (session == null) { // Should not happen startLoginActivity(uri) } else { From 5eeb545ae2a6b23ad58ca3cc6cd44654b0dd37e9 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 21 Jan 2021 15:20:19 +0300 Subject: [PATCH 09/51] Update url preview when the event is edited. --- CHANGES.md | 1 + .../sdk/api/session/media/MediaService.kt | 10 ++++--- .../session/media/DefaultMediaService.kt | 6 ++-- .../internal/session/media/UrlsExtractor.kt | 12 ++++---- .../home/room/detail/RoomDetailViewModel.kt | 2 +- .../timeline/url/PreviewUrlRetriever.kt | 29 +++++++++++++------ 6 files changed, 37 insertions(+), 23 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b2483df78d..9dd6b3a894 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ Improvements 🙌: 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) Translations 🗣: - 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..a7a7c93073 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,17 @@ 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. + * @param forceExtract Should be used for edited events. If true, URL will be extracted again even it is already in the cache. + * @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, forceExtract: Boolean = false): 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/internal/session/media/DefaultMediaService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt index 1a400ccfcf..574a97b191 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 @@ -21,6 +21,7 @@ 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.util.JsonDict import org.matrix.android.sdk.internal.util.getOrPut import javax.inject.Inject @@ -34,8 +35,9 @@ internal class DefaultMediaService @Inject constructor( // Cache of extracted URLs private val extractedUrlsCache = LruCache>(1_000) - override fun extractUrls(event: Event): List { - return extractedUrlsCache.getOrPut(event.cacheKey()) { urlsExtractor.extract(event) } + override fun extractUrls(event: TimelineEvent, forceExtract: Boolean): List { + if (forceExtract) extractedUrlsCache.remove(event.root.cacheKey()) + return extractedUrlsCache.getOrPut(event.root.cacheKey()) { urlsExtractor.extract(event) } } private fun Event.cacheKey() = "${eventId ?: ""}-${roomId ?: ""}" 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/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 ecfd1f85d3..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 @@ -1396,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/timeline/url/PreviewUrlRetriever.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlRetriever.kt index 695661feeb..0df5dc125c 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,46 @@ 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.hasBeenEdited class PreviewUrlRetriever(session: Session) { private val mediaService = session.mediaService() 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 synchronized(data) { - if (data[eventId] == null) { + val isEditedEvent = event.hasBeenEdited() + if (data[eventId] == null || isEditedEvent) { // Keep only the first URL for the moment - val url = mediaService.extractUrls(event) + val url = mediaService.extractUrls(event, forceExtract = isEditedEvent) .firstOrNull() ?.takeIf { it !in blockedUrl } if (url == null) { updateState(eventId, PreviewUrlUiState.NoUrl) - } else { + url + } else if (url != (data[eventId] as? PreviewUrlUiState.Data)?.url) { updateState(eventId, PreviewUrlUiState.Loading) + url + } else { + // Already handled + null } - url } else { // Already handled null @@ -96,8 +105,10 @@ class PreviewUrlRetriever(session: Session) { private fun updateState(eventId: String, state: PreviewUrlUiState) { data[eventId] = state // Notify the listener - listeners[eventId].orEmpty().forEach { - it.onStateUpdated(state) + uiHandler.post { + listeners[eventId].orEmpty().forEach { + it.onStateUpdated(state) + } } } From daf019b28807452df54b3d8efe59f95cff2d96c9 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Tue, 5 Jan 2021 00:58:55 -0500 Subject: [PATCH 10/51] Identity: Recompute hashes after M_INVALID_PEPPER When a new pepper is retrieved after an M_INVALID_PEPPER response, recompute hashes with that pepper, and send those new hashes in the next lookup attempt instead of reusing the original hashes. Signed-off-by: Andrew Ferrazzutti --- CHANGES.md | 1 + .../identity/IdentityBulkLookupTask.kt | 48 ++++++++++--------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b2483df78d..a8aabb213d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ Improvements 🙌: 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) + - When receiving a new pepper from identity server, use it on the next hash lookup (#2708) Translations 🗣: - 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..773d1066b5 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 @@ -46,6 +46,17 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( @UserId private val userId: String ) : IdentityBulkLookupTask { + 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) + ) + } + } + } + override suspend fun execute(params: IdentityBulkLookupTask.Params): List { val identityAPI = getIdentityApiAndEnsureTerms(identityApiProvider, userId) val identityData = identityStore.getIdentityData() ?: throw IdentityServiceError.NoIdentityServerConfigured @@ -63,33 +74,26 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( 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 lookupResult = lookUpInternal(identityAPI, params.threePids, hashDetailResponse, true) // Convert back to List - return handleSuccess(params.threePids, hashedAddresses, identityLookUpV2Response) + return handleSuccess(params.threePids, lookupResult.first, lookupResult.second) } private suspend fun lookUpInternal(identityAPI: IdentityAPI, - hashedAddresses: List, + threePids: List, hashDetailResponse: IdentityHashDetailResponse, - canRetry: Boolean): IdentityLookUpResponse { + canRetry: Boolean): Pair, IdentityLookUpResponse> { + val hashedAddresses = getHashedAddresses(threePids, hashDetailResponse.pepper) return try { - executeRequest(null) { - apiCall = identityAPI.lookup(IdentityLookUpParams( - hashedAddresses, - IdentityHashDetailResponse.ALGORITHM_SHA256, - hashDetailResponse.pepper - )) - } + Pair(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) { @@ -98,7 +102,7 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( // 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 */) } + .let { lookUpInternal(identityAPI, threePids, it, false /* Avoid infinite loop */) } } else { // Retrieve the new hash details val newHashDetailResponse = fetchAndStoreHashDetails(identityAPI) @@ -109,7 +113,7 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( throw IdentityServiceError.BulkLookupSha256NotSupported } - lookUpInternal(identityAPI, hashedAddresses, newHashDetailResponse, false /* Avoid infinite loop */) + lookUpInternal(identityAPI, threePids, newHashDetailResponse, false /* Avoid infinite loop */) } } else { // Other error From 883a7cecf0ab17a03d7b876995cc20b7c3013a07 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 22 Jan 2021 15:34:40 +0300 Subject: [PATCH 11/51] Fix viewbinding npe crashes. --- CHANGES.md | 1 + .../java/im/vector/app/ui/UiAllScreensSanityTest.kt | 2 ++ .../app/features/home/room/detail/RoomDetailFragment.kt | 5 ++++- .../app/features/home/room/detail/search/SearchFragment.kt | 4 ++-- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b2483df78d..d6e3a9b21a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ Improvements 🙌: 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) + - Crashes reported by PlayStore (new in 1.0.14) (#2707) Translations 🗣: - 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/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/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 -> From 25dbb3e9eac4d738afa5459f9f7100c62e793ffd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 22 Jan 2021 17:26:10 +0100 Subject: [PATCH 12/51] Fix bad copy/paste --- .../sdk/internal/session/identity/IdentityBulkLookupTask.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 773d1066b5..2d2e5f823d 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 @@ -107,9 +107,8 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( // Retrieve the new hash details val newHashDetailResponse = fetchAndStoreHashDetails(identityAPI) - if (hashDetailResponse.algorithms.contains(IdentityHashDetailResponse.ALGORITHM_SHA256).not()) { + 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 - // Also, what we have in cache is maybe outdated, the identity server maybe now support sha256 throw IdentityServiceError.BulkLookupSha256NotSupported } From 07ffd3ded37b4756f2d580ab6377c1daf2914287 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 22 Jan 2021 17:32:00 +0100 Subject: [PATCH 13/51] Improve code --- .../session/identity/IdentityBulkLookupTask.kt | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) 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 2d2e5f823d..ed206d237b 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 @@ -98,22 +98,19 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( // 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, threePids, it, false /* Avoid infinite loop */) } } else { // Retrieve the new hash details - val newHashDetailResponse = fetchAndStoreHashDetails(identityAPI) - - 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 */) + fetchAndStoreHashDetails(identityAPI) } + 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 From 887da0a3d61aa8c6b7175981b376501b01f53c3f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 22 Jan 2021 17:37:25 +0100 Subject: [PATCH 14/51] Improve code #2 --- .../session/identity/IdentityBulkLookupTask.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 ed206d237b..c064b81e4a 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 @@ -63,7 +63,8 @@ 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) } @@ -101,11 +102,11 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( val newHashDetailResponse = if (!failure.error.newLookupPepper.isNullOrEmpty()) { // Store it and use it right now hashDetailResponse.copy(pepper = failure.error.newLookupPepper) - .also { identityStore.setHashDetails(it) } } else { // Retrieve the new hash details - fetchAndStoreHashDetails(identityAPI) + 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 @@ -118,11 +119,10 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( } } - private suspend fun fetchAndStoreHashDetails(identityAPI: IdentityAPI): IdentityHashDetailResponse { - return executeRequest(null) { + private suspend fun fetchHashDetails(identityAPI: IdentityAPI): IdentityHashDetailResponse { + return executeRequest(null) { apiCall = identityAPI.hashDetails() } - .also { identityStore.setHashDetails(it) } } private fun handleSuccess(threePids: List, hashedAddresses: List, identityLookUpResponse: IdentityLookUpResponse): List { From 267ae457eeaee7a0f36ec3d33ebece01105e6ce2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 22 Jan 2021 17:38:17 +0100 Subject: [PATCH 15/51] Use const --- .../sdk/internal/session/identity/IdentityBulkLookupTask.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c064b81e4a..e429289a24 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 @@ -69,7 +69,7 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( 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 From a44d00a31c84ecfb9fb0b7a6b84ef7b08eb1ea48 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 22 Jan 2021 17:44:24 +0100 Subject: [PATCH 16/51] Create data class. --- .../identity/IdentityBulkLookupTask.kt | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) 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 e429289a24..ccee5f7d47 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 @@ -78,16 +78,21 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( val lookupResult = lookUpInternal(identityAPI, params.threePids, hashDetailResponse, true) // Convert back to List - return handleSuccess(params.threePids, lookupResult.first, lookupResult.second) + return handleSuccess(params.threePids, lookupResult) } + data class LookUpData( + val hashedAddresses: List, + val identityLookUpResponse: IdentityLookUpResponse + ) + private suspend fun lookUpInternal(identityAPI: IdentityAPI, threePids: List, hashDetailResponse: IdentityHashDetailResponse, - canRetry: Boolean): Pair, IdentityLookUpResponse> { + canRetry: Boolean): LookUpData { val hashedAddresses = getHashedAddresses(threePids, hashDetailResponse.pepper) return try { - Pair(hashedAddresses, + LookUpData(hashedAddresses, executeRequest(null) { apiCall = identityAPI.lookup(IdentityLookUpParams( hashedAddresses, @@ -125,9 +130,12 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( } } - 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 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("") + ) } } } From 401b5e2b7a38cd67924d24a633195c7d6a0ab783 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 22 Jan 2021 17:48:13 +0100 Subject: [PATCH 17/51] Move private fun --- .../identity/IdentityBulkLookupTask.kt | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) 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 ccee5f7d47..61d44a7a65 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 @@ -46,17 +46,6 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( @UserId private val userId: String ) : IdentityBulkLookupTask { - 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) - ) - } - } - } - override suspend fun execute(params: IdentityBulkLookupTask.Params): List { val identityAPI = getIdentityApiAndEnsureTerms(identityApiProvider, userId) val identityData = identityStore.getIdentityData() ?: throw IdentityServiceError.NoIdentityServerConfigured @@ -124,6 +113,17 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( } } + 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) + ) + } + } + } + private suspend fun fetchHashDetails(identityAPI: IdentityAPI): IdentityHashDetailResponse { return executeRequest(null) { apiCall = identityAPI.hashDetails() From b65fc4f46b19fe3399e1e555044efbf33eecae00 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 22 Jan 2021 17:59:56 +0100 Subject: [PATCH 18/51] rename val --- .../sdk/internal/session/identity/IdentityBulkLookupTask.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 61d44a7a65..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 @@ -64,10 +64,10 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( throw IdentityServiceError.BulkLookupSha256NotSupported } - val lookupResult = lookUpInternal(identityAPI, params.threePids, hashDetailResponse, true) + val lookUpData = lookUpInternal(identityAPI, params.threePids, hashDetailResponse, true) // Convert back to List - return handleSuccess(params.threePids, lookupResult) + return handleSuccess(params.threePids, lookUpData) } data class LookUpData( From 89cf1f32376e83f3f250cedd577570cd4cd6b521 Mon Sep 17 00:00:00 2001 From: gradle-update-robot Date: Sat, 23 Jan 2021 01:32:49 +0000 Subject: [PATCH 19/51] Update Gradle Wrapper from 6.8 to 6.8.1. Signed-off-by: gradle-update-robot --- gradle/wrapper/gradle-wrapper.properties | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 247e2b90ad..517ae0d4ce 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,6 @@ -#Fri Jan 15 11:30:47 CET 2021 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=a7ca23b3ccf265680f2bfd35f1f00b1424f4466292c7337c85d46c9641b3f053 +distributionSha256Sum=3db89524a3981819ff28c3f979236c1274a726e146ced0c8a2020417f9bc0782 +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-all.zip From 5a0d62db6f28c58110e0774a53fcadf2bd9e0810 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 25 Jan 2021 12:42:39 +0100 Subject: [PATCH 20/51] Cleanup (PR review) Also add some doc and add missing `internal` keyword --- .../room/timeline/EventContextResponse.kt | 32 +++++++++++++------ .../room/timeline/PaginationResponse.kt | 26 +++++++++++++-- .../session/room/timeline/TokenChunkEvent.kt | 2 +- .../room/timeline/TokenChunkEventPersistor.kt | 14 ++++---- .../session/room/uploads/GetUploadsTask.kt | 6 ++-- 5 files changed, 57 insertions(+), 23 deletions(-) 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 d76ba35280..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,20 +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 { - mutableListOf().apply { - eventsAfter?.let { addAll(it.reversed()) } - add(event) - eventsBefore?.let { addAll(it) } - } + 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 ff0c7fbbde..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 50cc50beb2..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 @@ -21,7 +21,7 @@ import org.matrix.android.sdk.api.session.events.model.Event internal interface TokenChunkEvent { val start: String? val end: String? - val events: List? + val events: 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 fb636a9314..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 @@ -124,7 +124,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri direction: PaginationDirection): Result { monarchy .awaitTransaction { realm -> - Timber.v("Start persisting ${receivedChunk.events?.size} events in $roomId towards $direction") + Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction") val nextToken: String? val prevToken: String? @@ -149,14 +149,14 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri } ?: ChunkEntity.create(realm, prevToken, nextToken) - if (receivedChunk.events.isNullOrEmpty() && !receivedChunk.hasMore()) { + if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) { handleReachEnd(realm, roomId, direction, currentChunk) } else { handlePagination(realm, roomId, direction, receivedChunk, currentChunk) } } - return if (receivedChunk.events.isNullOrEmpty()) { - if (receivedChunk.start != receivedChunk.end) { + return if (receivedChunk.events.isEmpty()) { + if (receivedChunk.hasMore()) { Result.SHOULD_FETCH_MORE } else { Result.REACHED_END @@ -189,7 +189,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri receivedChunk: TokenChunkEvent, currentChunk: ChunkEntity ) { - Timber.v("Add ${receivedChunk.events?.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}") + Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}") val roomMemberContentsByUser = HashMap() val eventList = receivedChunk.events val stateEvents = receivedChunk.stateEvents @@ -204,8 +204,8 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri roomMemberContentsByUser[stateEvent.stateKey] = stateEvent.content.toModel() } } - val eventIds = ArrayList(eventList?.size ?: 0) - eventList?.forEach { event -> + val eventIds = ArrayList(eventList.size) + eventList.forEach { event -> if (event.eventId == null || event.senderId == null) { return@forEach } 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 9d66474a73..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 @@ -95,7 +95,7 @@ internal class DefaultGetUploadsTask @Inject constructor( nextToken = chunk.end ?: "", hasMore = chunk.hasMore() ) - events = chunk.events ?: emptyList() + events = chunk.events } var uploadEvents = listOf() From 602ea3327bcd69ba8a196e6fa24b956ace924444 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 25 Jan 2021 15:11:42 +0100 Subject: [PATCH 21/51] URL preview: improve fix regarding message edition --- .../sdk/api/session/media/MediaService.kt | 3 +- .../session/room/timeline/TimelineEvent.kt | 11 +++++ .../session/media/DefaultMediaService.kt | 10 ++--- .../timeline/url/PreviewUrlRetriever.kt | 44 ++++++++++++------- 4 files changed, 44 insertions(+), 24 deletions(-) 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 a7a7c93073..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 @@ -24,10 +24,9 @@ interface MediaService { /** * Extract URLs from a TimelineEvent. * @param event TimelineEvent to extract the URL from. - * @param forceExtract Should be used for edited events. If true, URL will be extracted again even it is already in the cache. * @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: TimelineEvent, forceExtract: Boolean = false): 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/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/internal/session/media/DefaultMediaService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt index 574a97b191..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,10 +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 @@ -35,12 +35,12 @@ internal class DefaultMediaService @Inject constructor( // Cache of extracted URLs private val extractedUrlsCache = LruCache>(1_000) - override fun extractUrls(event: TimelineEvent, forceExtract: Boolean): List { - if (forceExtract) extractedUrlsCache.remove(event.root.cacheKey()) - return extractedUrlsCache.getOrPut(event.root.cacheKey()) { urlsExtractor.extract(event) } + 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/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 0df5dc125c..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 @@ -24,12 +24,19 @@ 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.room.timeline.TimelineEvent -import org.matrix.android.sdk.api.session.room.timeline.hasBeenEdited +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()) @@ -38,19 +45,22 @@ class PreviewUrlRetriever(session: Session) { fun getPreviewUrl(event: TimelineEvent, coroutineScope: CoroutineScope) { val eventId = event.root.eventId ?: return + val latestEventId = event.getLatestEventId() synchronized(data) { - val isEditedEvent = event.hasBeenEdited() - if (data[eventId] == null || isEditedEvent) { + 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, forceExtract = isEditedEvent) + val url = mediaService.extractUrls(event) .firstOrNull() ?.takeIf { it !in blockedUrl } if (url == null) { - updateState(eventId, PreviewUrlUiState.NoUrl) - url - } else if (url != (data[eventId] as? PreviewUrlUiState.Data)?.url) { - updateState(eventId, PreviewUrlUiState.Loading) + 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 { // Already handled @@ -73,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)) } } ) @@ -95,15 +105,15 @@ 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 uiHandler.post { listeners[eventId].orEmpty().forEach { @@ -118,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) } } From 128d3845b979068b0530a8191d77e46628964225 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 25 Jan 2021 15:19:01 +0100 Subject: [PATCH 22/51] Small rework --- .../room/relation/DefaultRelationService.kt | 2 +- .../room/relation/FetchEditHistoryTask.kt | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) 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 b27cbbb0b2..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 @@ -141,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() From f64db7f5f32426856c70c5ec99f5aee1cddfaf08 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 25 Jan 2021 16:03:31 +0100 Subject: [PATCH 23/51] Fix test compilation --- .../session/media/UrlsExtractorTest.kt | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) 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..ad87ae03b1 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 @@ -105,4 +114,19 @@ internal class UrlsExtractorTest : InstrumentedTest { body = body ).toContent() ) + + private fun Event.toFakeTimelineEvent(): TimelineEvent { + return TimelineEvent( + root = this, + localId = 0L, + eventId = "", + displayIndex = 0, + senderInfo = SenderInfo( + userId = "", + displayName = null, + isUniqueDisplayName = true, + avatarUrl = null + ) + ) + } } From d6a5b9fb4825fcbe2f661808b698e8baeab89187 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 25 Jan 2021 16:26:58 +0100 Subject: [PATCH 24/51] fulfill assertion about eventId --- .../android/sdk/internal/session/media/UrlsExtractorTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 ad87ae03b1..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 @@ -108,6 +108,7 @@ internal class UrlsExtractorTest : InstrumentedTest { } private fun createEvent(body: String): Event = Event( + eventId = "!fake", type = EventType.MESSAGE, content = MessageTextContent( msgType = MessageType.MSGTYPE_TEXT, @@ -119,7 +120,7 @@ internal class UrlsExtractorTest : InstrumentedTest { return TimelineEvent( root = this, localId = 0L, - eventId = "", + eventId = eventId!!, displayIndex = 0, senderInfo = SenderInfo( userId = "", From f2f4d325eb67afc0262b97b44ecc30bb86637bc4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 25 Jan 2021 20:57:38 +0100 Subject: [PATCH 25/51] Bugfix: remove duplicate "More" section --- .../features/roommemberprofile/RoomMemberProfileController.kt | 2 -- 1 file changed, 2 deletions(-) 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, From afa3149504abc0b26c4c7656922e8364d491bf91 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 25 Jan 2021 23:09:14 +0100 Subject: [PATCH 26/51] Use lifecycleScope rather than an Empty ViewModel --- .../im/vector/app/core/platform/EmptyState.kt | 23 ---------------- .../app/core/platform/EmptyViewModel.kt | 26 ------------------- .../im/vector/app/features/MainActivity.kt | 12 +++------ .../app/features/link/LinkHandlerActivity.kt | 8 ++---- 4 files changed, 6 insertions(+), 63 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/core/platform/EmptyState.kt delete mode 100644 vector/src/main/java/im/vector/app/core/platform/EmptyViewModel.kt diff --git a/vector/src/main/java/im/vector/app/core/platform/EmptyState.kt b/vector/src/main/java/im/vector/app/core/platform/EmptyState.kt deleted file mode 100644 index c58532c6e0..0000000000 --- a/vector/src/main/java/im/vector/app/core/platform/EmptyState.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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.core.platform - -import com.airbnb.mvrx.MvRxState - -data class EmptyState( - val dummy: Int = 0 -) : MvRxState diff --git a/vector/src/main/java/im/vector/app/core/platform/EmptyViewModel.kt b/vector/src/main/java/im/vector/app/core/platform/EmptyViewModel.kt deleted file mode 100644 index 420c58d44a..0000000000 --- a/vector/src/main/java/im/vector/app/core/platform/EmptyViewModel.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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.core.platform - -/** - * Mainly used to get a viewModelScope - */ -class EmptyViewModel(initialState: EmptyState) : VectorViewModel(initialState) { - override fun handle(action: EmptyAction) { - // N/A - } -} 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 f8b8229e70..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,15 +22,13 @@ import android.os.Bundle import android.os.Parcelable import androidx.appcompat.app.AlertDialog import androidx.lifecycle.Lifecycle -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.viewModel +import androidx.lifecycle.lifecycleScope import com.bumptech.glide.Glide import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.startSyncing -import im.vector.app.core.platform.EmptyViewModel import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.deleteAllFiles import im.vector.app.databinding.FragmentLoadingBinding @@ -83,8 +81,6 @@ class MainActivity : VectorBaseActivity(), UnlockedActiv } } - private val emptyViewModel: EmptyViewModel by viewModel() - override fun getBinding() = FragmentLoadingBinding.inflate(layoutInflater) private lateinit var args: MainActivityArgs @@ -150,7 +146,7 @@ class MainActivity : VectorBaseActivity(), UnlockedActiv } when { args.isAccountDeactivated -> { - emptyViewModel.viewModelScope.launch { + lifecycleScope.launch { // Just do the local cleanup Timber.w("Account deactivated, start app") sessionHolder.clearActiveSession() @@ -159,7 +155,7 @@ class MainActivity : VectorBaseActivity(), UnlockedActiv } } args.clearCredentials -> { - emptyViewModel.viewModelScope.launch { + lifecycleScope.launch { try { session.signOut(!args.isUserLoggedOut) Timber.w("SIGN_OUT: success, start app") @@ -172,7 +168,7 @@ class MainActivity : VectorBaseActivity(), UnlockedActiv } } args.clearCache -> { - emptyViewModel.viewModelScope.launch { + lifecycleScope.launch { try { session.clearCache() doLocalCleanup(clearPreferences = false) 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 7183f27980..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,13 +19,11 @@ package im.vector.app.features.link import android.content.Intent import android.net.Uri import androidx.appcompat.app.AlertDialog -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.viewModel +import androidx.lifecycle.lifecycleScope import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter -import im.vector.app.core.platform.EmptyViewModel import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.toast import im.vector.app.databinding.ActivityProgressBinding @@ -48,8 +46,6 @@ class LinkHandlerActivity : VectorBaseActivity() { @Inject lateinit var errorFormatter: ErrorFormatter @Inject lateinit var permalinkHandler: PermalinkHandler - private val emptyViewModel: EmptyViewModel by viewModel() - override fun injectWith(injector: ScreenComponent) { injector.inject(this) } @@ -155,7 +151,7 @@ class LinkHandlerActivity : VectorBaseActivity() { // Should not happen startLoginActivity(uri) } else { - emptyViewModel.viewModelScope.launch { + lifecycleScope.launch { try { session.signOut(true) Timber.d("## displayAlreadyLoginPopup(): logout succeeded") From c794843bb25ad5d29b0fe2597adc1517002f5517 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 26 Jan 2021 17:16:13 +0100 Subject: [PATCH 27/51] Inject the context in the constructor --- .../sdk/internal/session/content/ImageCompressor.kt | 7 +++---- .../sdk/internal/session/content/UploadContentWorker.kt | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) 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/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 { From 06be3e691de4e7e89969a253eeb915e44106db45 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 1 Feb 2021 18:13:26 +0300 Subject: [PATCH 28/51] Support $matrix_widget_id parameter. --- CHANGES.md | 1 + .../sdk/internal/session/widgets/helper/WidgetFactory.kt | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 90c2e05c65..537e3aef7b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,7 @@ Bugfix 🐛: - 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) Translations 🗣: - 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) From 1244d00b31fce45734a7519bb792ed8692b7bfa1 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 27 Jan 2021 09:52:18 +0100 Subject: [PATCH 29/51] SSO UIA --- .../auth/UserInteractiveAuthInterceptor.kt | 48 ++++ .../matrix/android/sdk/api/session/Session.kt | 2 + .../sdk/api/session/crypto/CryptoService.kt | 3 +- .../crosssigning/CrossSigningService.kt | 4 +- .../internal/crypto/DefaultCryptoService.kt | 5 +- .../DefaultCrossSigningService.kt | 16 +- .../crypto/model/rest/DefaultBaseAuth.kt | 32 +++ .../crypto/model/rest/DeleteDeviceParams.kt | 2 +- .../crypto/model/rest/TokenBasedAuth.kt | 69 ++++++ .../internal/crypto/model/rest/UIABaseAuth.kt | 31 +++ .../model/rest/UploadSigningKeysBody.kt | 2 +- .../crypto/model/rest/UserPasswordAuth.kt | 16 +- .../internal/crypto/tasks/DeleteDeviceTask.kt | 53 ++++- .../tasks/DeleteDeviceWithUserPasswordTask.kt | 4 +- .../tasks/InitializeCrossSigningTask.kt | 58 ++++- .../crypto/tasks/UploadSigningKeysTask.kt | 36 +-- .../sdk/internal/session/DefaultSession.kt | 13 ++ .../src/main/res/values/strings.xml | 1 + vector/src/main/AndroidManifest.xml | 21 ++ .../im/vector/app/core/di/ScreenComponent.kt | 2 + .../core/ui/list/GenericPositiveButtonItem.kt | 63 ++++++ .../app/features/auth/PromptFragment.kt | 95 ++++++++ .../vector/app/features/auth/ReAuthActions.kt | 27 +++ .../app/features/auth/ReAuthActivity.kt | 213 ++++++++++++++++++ .../vector/app/features/auth/ReAuthEvents.kt | 25 ++ .../vector/app/features/auth/ReAuthState.kt | 35 +++ .../app/features/auth/ReAuthViewModel.kt | 79 +++++++ .../recover/BootstrapCrossSigningTask.kt | 22 +- .../features/home/HomeActivityViewModel.kt | 44 +++- .../VectorSettingsSecurityPrivacyFragment.kt | 4 - .../CrossSigningSettingsAction.kt | 7 +- .../CrossSigningSettingsController.kt | 36 ++- .../CrossSigningSettingsFragment.kt | 49 +++- .../CrossSigningSettingsViewEvents.kt | 4 + .../CrossSigningSettingsViewModel.kt | 103 ++++++++- .../CrossSigningSettingsViewState.kt | 3 +- .../settings/devices/DevicesAction.kt | 6 +- .../settings/devices/DevicesViewEvents.kt | 6 +- .../settings/devices/DevicesViewModel.kt | 174 +++++++------- .../devices/VectorSettingsDevicesFragment.kt | 46 ++-- .../res/layout/fragment_reauth_confirm.xml | 99 ++++++++ .../main/res/layout/item_positive_button.xml | 19 ++ vector/src/main/res/values/strings.xml | 4 + 43 files changed, 1381 insertions(+), 200 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserInteractiveAuthInterceptor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DefaultBaseAuth.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/TokenBasedAuth.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UIABaseAuth.kt create mode 100644 vector/src/main/java/im/vector/app/core/ui/list/GenericPositiveButtonItem.kt create mode 100644 vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/auth/ReAuthActions.kt create mode 100644 vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt create mode 100644 vector/src/main/java/im/vector/app/features/auth/ReAuthEvents.kt create mode 100644 vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt create mode 100644 vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt create mode 100644 vector/src/main/res/layout/fragment_reauth_confirm.xml create mode 100644 vector/src/main/res/layout/item_positive_button.xml 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..11cf2d2cfb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserInteractiveAuthInterceptor.kt @@ -0,0 +1,48 @@ +/* + * 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.internal.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth +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 provider 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, promise : Continuation) +} 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..bc6fd3adad 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/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/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..e85cea30a9 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 @@ -147,11 +147,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 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..7d55f3b6cb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DefaultBaseAuth.kt @@ -0,0 +1,32 @@ +/* + * 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 + +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/TokenBasedAuth.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/TokenBasedAuth.kt new file mode 100644 index 0000000000..479ac3b0c5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/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.internal.crypto.model.rest + +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/crypto/model/rest/UIABaseAuth.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UIABaseAuth.kt new file mode 100644 index 0000000000..246bec42e6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UIABaseAuth.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.model.rest + +interface UIABaseAuth { + /** + * This is a session identifier that the client must pass back to the homeserver, + * if one is provided, in subsequent attempts to authenticate in the same API call. + */ + val session: String? + + fun hasAuthInfo(): Boolean + + fun copyWithSession(session: String): UIABaseAuth + + fun asMap() : Map +} 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/model/rest/UserPasswordAuth.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UserPasswordAuth.kt index ba8b34096c..0db6ecb7ab 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/internal/crypto/model/rest/UserPasswordAuth.kt @@ -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/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt index 8f1569a037..3c1721b06b 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,24 @@ package org.matrix.android.sdk.internal.crypto.tasks +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.internal.crypto.api.CryptoApi import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams +import org.matrix.android.sdk.internal.crypto.model.rest.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 +import kotlin.coroutines.suspendCoroutine internal interface DeleteDeviceTask : Task { data class Params( - val deviceId: String + val deviceId: String, + val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?, + val userAuthParam: UIABaseAuth? ) } @@ -39,12 +45,49 @@ 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)) { + Timber.d("## UIA: propagate failure") + throw throwable + } + } + } + + private suspend fun handleUIA(failure: Throwable, params: DeleteDeviceTask.Params): Boolean { + Timber.d("## UIA: check error delete device ${failure.message}") + if (failure is Failure.OtherServerError && failure.httpCode == 401) { + Timber.d("## UIA: error can be passed to interceptor") + // give a chance to the reauth helper? + val flowResponse = failure.toRegistrationFlowResponse() + ?: return false.also { + Timber.d("## UIA: failed to parse flow response") + } + + Timber.d("## UIA: type = ${flowResponse.flows}") + Timber.d("## UIA: has interceptor = ${params.userInteractiveAuthInterceptor != null}") + + Timber.d("## UIA: delegate to interceptor...") + val authUpdate = try { + suspendCoroutine { continuation -> + params.userInteractiveAuthInterceptor!!.performStage(flowResponse, continuation) + } + } catch (failure: Throwable) { + Timber.w(failure, "## UIA: failed to participate") + return false + } + + Timber.d("## UIA: delete device updated auth $authUpdate") + return try { + execute(params.copy(userAuthParam = authUpdate)) + true + } catch (failure: Throwable) { + handleUIA(failure, params) + } + } else { + Timber.d("## UIA: not a UIA error") + return false } } } 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..3cfe26fa82 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 @@ -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..e261828e94 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,24 +17,28 @@ package org.matrix.android.sdk.internal.crypto.tasks import dagger.Lazy +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.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder import org.matrix.android.sdk.internal.crypto.crosssigning.canonicalSignable import org.matrix.android.sdk.internal.crypto.crosssigning.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.UIABaseAuth 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 import org.matrix.olm.OlmPkSigning import timber.log.Timber import javax.inject.Inject +import kotlin.coroutines.suspendCoroutine internal interface InitializeCrossSigningTask : Task { data class Params( - val authParams: UserPasswordAuth? + val interactiveAuthInterceptor: UserInteractiveAuthInterceptor? ) data class Result( @@ -117,10 +121,18 @@ 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, uploadSigningKeysParams)) { + Timber.d("## UIA: propagate failure") + throw failure + } + } // Sign the current device with SSK val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder() @@ -169,4 +181,42 @@ internal class DefaultInitializeCrossSigningTask @Inject constructor( selfSigningPkOlm?.releaseSigning() } } + + private suspend fun handleUIA(failure: Throwable, + params: InitializeCrossSigningTask.Params, + uploadSigningKeysParams: UploadSigningKeysTask.Params): Boolean { + Timber.d("## UIA: check error initialize xsigning ${failure.message}") + if (failure is Failure.OtherServerError && failure.httpCode == 401) { + Timber.d("## UIA: error can be passed to interceptor") + // give a chance to the reauth helper? + val flowResponse = failure.toRegistrationFlowResponse() + ?: return false.also { + Timber.d("## UIA: failed to parse flow response") + } + + Timber.d("## UIA: type = ${flowResponse.flows}") + Timber.d("## UIA: has interceptor = ${params.interactiveAuthInterceptor != null}") + + Timber.d("## UIA: delegate to interceptor...") + val authUpdate = try { + suspendCoroutine { continuation -> + params.interactiveAuthInterceptor!!.performStage(flowResponse, continuation) + } + } catch (failure: Throwable) { + Timber.w(failure, "## UIA: failed to participate") + return false + } + + Timber.d("## UIA: initialize xsigning updated auth $authUpdate") + try { + uploadSigningKeysTask.execute(uploadSigningKeysParams.copy(userAuthParam = authUpdate)) + return true + } catch (failure: Throwable) { + return handleUIA(failure, params, uploadSigningKeysParams) + } + } else { + Timber.d("## UIA: not a UIA error") + return false + } + } } 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..1723e21ed4 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.internal.crypto.model.rest.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%s is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys. + Failed to set up Cross Signing diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 0341059674..1028f200af 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -242,6 +242,27 @@ + + + + + + + + + + + + + + + + + () { + + @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/auth/PromptFragment.kt b/vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt new file mode 100644 index 0000000000..6556e6ae65 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt @@ -0,0 +1,95 @@ +/* + * 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 -> + if (state.flowType == LoginFlowTypes.SSO) { + viewModel.handle(ReAuthActions.StartSSOFallback) + } else if (state.flowType == 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) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthActions.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthActions.kt new file mode 100644 index 0000000000..036afda405 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthActions.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.auth + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class ReAuthActions : VectorViewModelAction { + object StartSSOFallback : ReAuthActions() + object FallBackPageLoaded : ReAuthActions() + object FallBackPageClosed : ReAuthActions() + object TogglePassVisibility : ReAuthActions() + data class ReAuthWithPass(val password: String) : ReAuthActions() +} 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..0c27911e0e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt @@ -0,0 +1,213 @@ +/* + * 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.internal.auth.registration.RegistrationFlowResponse +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? + ) : 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 :/ + + 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.password) + }) + 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() = withState(sharedViewModel) { state -> + super.onStart() + + 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" + + fun newIntent(context: Context, fromError: RegistrationFlowResponse, reasonTitle: String?): Intent { + val authType = if (fromError.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true }) { + LoginFlowTypes.PASSWORD + } else if (fromError.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.SSO) == true }) { + 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)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthEvents.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthEvents.kt new file mode 100644 index 0000000000..303d87a4c7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthEvents.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.auth + +import im.vector.app.core.platform.VectorViewEvents + +sealed class ReAuthEvents : VectorViewEvents { + data class OpenSsoURl(val url: String) : ReAuthEvents() + object Dismiss : ReAuthEvents() + data class PasswordFinishSuccess(val password: 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..f80c9acdd2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt @@ -0,0 +1,35 @@ +/* + * 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 +) : MvRxState { + constructor(args: ReAuthActivity.Args) : this( + args.title, + args.session, + args.flowType + ) + + 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..d29bf2828d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt @@ -0,0 +1,79 @@ +/* + * 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.AuthenticationService +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import org.matrix.android.sdk.api.session.Session + +class ReAuthViewModel @AssistedInject constructor( + @Assisted val initialState: ReAuthState, + private val session: Session, + private val authenticationService: AuthenticationService +) : 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) { + 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 -> { + _viewEvents.post(ReAuthEvents.PasswordFinishSuccess(action.password)) + } + } + } +} 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..ebc8239765 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,6 +20,7 @@ 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.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError @@ -33,16 +34,21 @@ import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec +import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo 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.UIABaseAuth import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth import org.matrix.android.sdk.internal.util.awaitCallback import timber.log.Timber +import java.lang.UnsupportedOperationException import java.util.UUID import javax.inject.Inject +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume sealed class BootstrapResult { @@ -101,7 +107,21 @@ class BootstrapCrossSigningTask @Inject constructor( try { awaitCallback { - crossSigningService.initializeCrossSigning(params.userPasswordAuth, it) + crossSigningService.initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, promise: Continuation) { + if (flowResponse.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true) { + val updatedAuth = params.userPasswordAuth?.copy(session = flowResponse.session) + if (updatedAuth == null) { + promise.resumeWith(Result.failure(UnsupportedOperationException())) + } else { + promise.resume(updatedAuth) + } + } else { + promise.resumeWith(Result.failure(UnsupportedOperationException())) + } + } + }, + it) } if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) { return BootstrapResult.SuccessCrossSigningOnly 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 45fce13ab9..5ba617b379 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 @@ -21,8 +21,8 @@ import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject 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 @@ -33,17 +33,23 @@ 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.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.internal.auth.registration.RegistrationFlowResponse 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.UIABaseAuth import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth 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, @@ -122,7 +128,7 @@ 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() @@ -152,11 +158,19 @@ class HomeActivityViewModel @AssistedInject constructor( // 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 - ), + object : UserInteractiveAuthInterceptor { + override fun performStage(flow: RegistrationFlowResponse, promise: Continuation) { + if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true) { + promise.resume( + UserPasswordAuth( + session = flow.session, + user = session.myUserId, + password = password + ) + ) + } else promise.resumeWith(Result.failure(UnsupportedOperationException())) + } + }, callback = NoOpMatrixCallback() ) } @@ -236,11 +250,17 @@ class HomeActivityViewModel @AssistedInject constructor( // 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 - ), + object : UserInteractiveAuthInterceptor { + override fun performStage(flow: RegistrationFlowResponse, promise: Continuation) { + if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true) { + UserPasswordAuth( + session = flow.session, + user = session.myUserId, + password = password + ) + } else null + } + }, callback = NoOpMatrixCallback() ) } 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/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..4625c21cdd 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,52 @@ 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, 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 +122,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..8da5da4c7b 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.internal.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) : 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 fdf5d611fa..06fa930ef1 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,25 +15,45 @@ */ 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 dagger.assisted.Assisted -import dagger.assisted.AssistedInject 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.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.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes 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.auth.registration.RegistrationFlowResponse 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.crypto.model.rest.UIABaseAuth +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +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>>( @@ -58,15 +78,82 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat } } + 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 + override fun handle(action: CrossSigningSettingsAction) = withState { state -> + when (action) { + CrossSigningSettingsAction.InitializeCrossSigning -> { + _viewEvents.post(CrossSigningSettingsViewEvents.ShowModalWaitingView(null)) + viewModelScope.launch(Dispatchers.IO) { + try { + awaitCallback { + session.cryptoService().crossSigningService().initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flow: RegistrationFlowResponse, promise: Continuation) { + Timber.d("## UIA : initializeCrossSigning UIA") + if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true && reAuthHelper.data != 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(flow)) + pendingAuth = DefaultBaseAuth(session = flow.session) + uiaContinuation = promise + } + } + }, it) + } + } catch (failure: Throwable) { + handleInitializeXSigningError(failure) + } finally { + _viewEvents.post(CrossSigningSettingsViewEvents.HideModalWaitingView) + } + } + Unit + } + is CrossSigningSettingsAction.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 CrossSigningSettingsAction.PasswordAuthDone -> { + uiaContinuation?.resume( + UserPasswordAuth( + session = pendingAuth?.session, + password = action.password, + 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/crosssigning/CrossSigningSettingsViewState.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewState.kt index 8a371ada68..aef27c598e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewState.kt @@ -23,5 +23,6 @@ data class CrossSigningSettingsViewState( val crossSigningInfo: MXCrossSigningInfo? = null, val xSigningIsEnableInAccount: Boolean = false, val xSigningKeysAreTrusted: Boolean = false, - val xSigningKeyCanSign: Boolean = true + val xSigningKeyCanSign: Boolean = true, +// val pendingAuthSession: String? = null ) : MvRxState 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..5644d1c63e 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.internal.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) : 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 eb034530ef..95b1619b21 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 @@ -27,16 +27,20 @@ import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory -import im.vector.app.core.error.SsoFlowNotSupportedYet +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.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 @@ -44,13 +48,20 @@ 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.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel 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.internal.crypto.model.rest.UIABaseAuth +import org.matrix.android.sdk.internal.crypto.model.rest.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 = "", @@ -70,9 +81,14 @@ 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 { + var uiaContinuation: Continuation? = null + var pendingAuth: UIABaseAuth? = null + @AssistedFactory interface Factory { fun create(initialState: DevicesViewState): DevicesViewModel @@ -87,10 +103,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 { @@ -187,15 +199,44 @@ class DevicesViewModel @AssistedInject constructor( override fun handle(action: DevicesAction) { 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.Refresh -> queryRefreshDevicesList() + is DevicesAction.Delete -> handleDelete(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 -> { + uiaContinuation?.resume( + UserPasswordAuth( + session = pendingAuth?.session, + password = action.password, + 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 + } } } @@ -285,95 +326,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(flow: RegistrationFlowResponse, promise: Continuation) { + Timber.d("## UIA : deleteDevice UIA") + if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true && reAuthHelper.data != 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(flow)) + pendingAuth = DefaultBaseAuth(session = flow.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..e4bc109d58 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 -> maybeShowDeleteDeviceWithPasswordDialog(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() @@ -154,17 +150,31 @@ class VectorSettingsDevicesFragment @Inject constructor( .show() } + 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) + } + } + /** * 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 fun maybeShowDeleteDeviceWithPasswordDialog(reAuthReq: DevicesViewEvents.RequestReAuth) { + ReAuthActivity.newIntent(requireContext(), reAuthReq.registrationFlowResponse, getString(R.string.devices_delete_dialog_title)).let { intent -> + reAuthActivityResultLauncher.launch(intent) } } diff --git a/vector/src/main/res/layout/fragment_reauth_confirm.xml b/vector/src/main/res/layout/fragment_reauth_confirm.xml new file mode 100644 index 0000000000..b8f1a57ae3 --- /dev/null +++ b/vector/src/main/res/layout/fragment_reauth_confirm.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_positive_button.xml b/vector/src/main/res/layout/item_positive_button.xml new file mode 100644 index 0000000000..cdee239e59 --- /dev/null +++ b/vector/src/main/res/layout/item_positive_button.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 1f282115e0..5f9f801887 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2792,4 +2792,8 @@ Discard changes Matrix Link + + Re-Authentication Needed + Element requires you to enter your credentials to perform this action. + Failed to authenticate From da16ec0af36aeeb36da913f7118b09117def8b51 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 1 Feb 2021 14:31:23 +0100 Subject: [PATCH 30/51] UIA fixes + better error support --- .../auth/UserInteractiveAuthInterceptor.kt | 2 +- .../android/sdk/api/failure/Extensions.kt | 10 + .../android/sdk/api/failure/MatrixError.kt | 14 +- .../sdk/internal/auth/registration/UIAExt.kt | 58 +++++ .../internal/crypto/tasks/DeleteDeviceTask.kt | 46 +--- .../tasks/InitializeCrossSigningTask.kt | 48 +--- .../im/vector/app/core/di/FragmentModule.kt | 6 +- .../app/features/auth/PromptFragment.kt | 46 +++- .../app/features/auth/ReAuthActivity.kt | 26 +- .../vector/app/features/auth/ReAuthState.kt | 6 +- .../BootstrapAccountPasswordFragment.kt | 110 --------- .../crypto/recover/BootstrapActions.kt | 6 +- .../crypto/recover/BootstrapBottomSheet.kt | 37 ++- .../recover/BootstrapCrossSigningTask.kt | 43 +--- .../crypto/recover/BootstrapReAuthFragment.kt | 86 +++++++ .../recover/BootstrapSharedViewModel.kt | 230 +++++++++--------- .../features/crypto/recover/BootstrapStep.kt | 6 +- .../crypto/recover/BootstrapViewEvents.kt | 2 + .../features/home/HomeActivityViewModel.kt | 9 +- .../features/navigation/DefaultNavigator.kt | 9 +- .../CrossSigningSettingsFragment.kt | 5 +- .../CrossSigningSettingsViewEvents.kt | 2 +- .../CrossSigningSettingsViewModel.kt | 7 +- .../CrossSigningSettingsViewState.kt | 3 +- .../settings/devices/DevicesViewEvents.kt | 2 +- .../settings/devices/DevicesViewModel.kt | 7 +- .../devices/VectorSettingsDevicesFragment.kt | 5 +- .../signout/ServerBackupStatusViewModel.kt | 6 +- .../res/layout/fragment_bootstrap_reauth.xml | 68 ++++++ .../res/layout/fragment_reauth_confirm.xml | 20 +- 30 files changed, 524 insertions(+), 401 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/UIAExt.kt delete mode 100644 vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapAccountPasswordFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthFragment.kt create mode 100644 vector/src/main/res/layout/fragment_bootstrap_reauth.xml 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 index 11cf2d2cfb..5c076b3f4b 100644 --- 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 @@ -44,5 +44,5 @@ interface UserInteractiveAuthInterceptor { * Updated auth should be provider 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, promise : Continuation) + fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) } 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..8d8df6ff82 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 @@ -53,6 +53,16 @@ fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? { .adapter(RegistrationFlowResponse::class.java) .fromJson(this.errorBody) } + } else if (this is Failure.ServerError && 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/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/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..23273178e5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/UIAExt.kt @@ -0,0 +1,58 @@ +/* + * 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.internal.crypto.model.rest.UIABaseAuth +import timber.log.Timber +import kotlin.coroutines.suspendCoroutine + +fun RegistrationFlowResponse.nextUncompletedStage(flowIndex: Int = 0): String? { + val completed = completedStages ?: emptyList() + return flows?.getOrNull(flowIndex)?.stages?.firstOrNull { completed.contains(it).not() } +} + +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/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt index 3c1721b06b..6e7eb438ee 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 @@ -17,8 +17,7 @@ package org.matrix.android.sdk.internal.crypto.tasks 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.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.internal.crypto.model.rest.UIABaseAuth @@ -27,7 +26,6 @@ import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task import timber.log.Timber import javax.inject.Inject -import kotlin.coroutines.suspendCoroutine internal interface DeleteDeviceTask : Task { data class Params( @@ -48,46 +46,14 @@ internal class DefaultDeleteDeviceTask @Inject constructor( apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams(params.userAuthParam?.asMap())) } } catch (throwable: Throwable) { - if (params.userInteractiveAuthInterceptor == null || !handleUIA(throwable, params)) { + if (params.userInteractiveAuthInterceptor == null + || !handleUIA(throwable, params.userInteractiveAuthInterceptor) { auth -> + execute(params.copy(userAuthParam = auth)) + } + ) { Timber.d("## UIA: propagate failure") throw throwable } } } - - private suspend fun handleUIA(failure: Throwable, params: DeleteDeviceTask.Params): Boolean { - Timber.d("## UIA: check error delete device ${failure.message}") - if (failure is Failure.OtherServerError && failure.httpCode == 401) { - Timber.d("## UIA: error can be passed to interceptor") - // give a chance to the reauth helper? - val flowResponse = failure.toRegistrationFlowResponse() - ?: return false.also { - Timber.d("## UIA: failed to parse flow response") - } - - Timber.d("## UIA: type = ${flowResponse.flows}") - Timber.d("## UIA: has interceptor = ${params.userInteractiveAuthInterceptor != null}") - - Timber.d("## UIA: delegate to interceptor...") - val authUpdate = try { - suspendCoroutine { continuation -> - params.userInteractiveAuthInterceptor!!.performStage(flowResponse, continuation) - } - } catch (failure: Throwable) { - Timber.w(failure, "## UIA: failed to participate") - return false - } - - Timber.d("## UIA: delete device updated auth $authUpdate") - return try { - execute(params.copy(userAuthParam = authUpdate)) - true - } catch (failure: Throwable) { - handleUIA(failure, params) - } - } else { - Timber.d("## UIA: not a UIA error") - return false - } - } } 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 e261828e94..5856d705a8 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 @@ -18,15 +18,13 @@ package org.matrix.android.sdk.internal.crypto.tasks import dagger.Lazy 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.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 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.UIABaseAuth import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.task.Task @@ -34,7 +32,6 @@ import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.olm.OlmPkSigning import timber.log.Timber import javax.inject.Inject -import kotlin.coroutines.suspendCoroutine internal interface InitializeCrossSigningTask : Task { data class Params( @@ -128,7 +125,10 @@ internal class DefaultInitializeCrossSigningTask @Inject constructor( try { uploadSigningKeysTask.execute(uploadSigningKeysParams) } catch (failure: Throwable) { - if (params.interactiveAuthInterceptor == null || !handleUIA(failure, params, uploadSigningKeysParams)) { + if (params.interactiveAuthInterceptor == null || + !handleUIA(failure, params.interactiveAuthInterceptor) { authUpdate -> + uploadSigningKeysTask.execute(uploadSigningKeysParams.copy(userAuthParam = authUpdate)) + }) { Timber.d("## UIA: propagate failure") throw failure } @@ -181,42 +181,4 @@ internal class DefaultInitializeCrossSigningTask @Inject constructor( selfSigningPkOlm?.releaseSigning() } } - - private suspend fun handleUIA(failure: Throwable, - params: InitializeCrossSigningTask.Params, - uploadSigningKeysParams: UploadSigningKeysTask.Params): Boolean { - Timber.d("## UIA: check error initialize xsigning ${failure.message}") - if (failure is Failure.OtherServerError && failure.httpCode == 401) { - Timber.d("## UIA: error can be passed to interceptor") - // give a chance to the reauth helper? - val flowResponse = failure.toRegistrationFlowResponse() - ?: return false.also { - Timber.d("## UIA: failed to parse flow response") - } - - Timber.d("## UIA: type = ${flowResponse.flows}") - Timber.d("## UIA: has interceptor = ${params.interactiveAuthInterceptor != null}") - - Timber.d("## UIA: delegate to interceptor...") - val authUpdate = try { - suspendCoroutine { continuation -> - params.interactiveAuthInterceptor!!.performStage(flowResponse, continuation) - } - } catch (failure: Throwable) { - Timber.w(failure, "## UIA: failed to participate") - return false - } - - Timber.d("## UIA: initialize xsigning updated auth $authUpdate") - try { - uploadSigningKeysTask.execute(uploadSigningKeysParams.copy(userAuthParam = authUpdate)) - return true - } catch (failure: Throwable) { - return handleUIA(failure, params, uploadSigningKeysParams) - } - } else { - Timber.d("## UIA: not a UIA error") - return false - } - } } diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index 407aa2fc73..1e257c169e 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -28,7 +28,7 @@ import im.vector.app.features.crypto.keysbackup.settings.KeysBackupSettingsFragm import im.vector.app.features.crypto.quads.SharedSecuredStorageKeyFragment import im.vector.app.features.crypto.quads.SharedSecuredStoragePassphraseFragment import im.vector.app.features.crypto.quads.SharedSecuredStorageResetAllFragment -import im.vector.app.features.crypto.recover.BootstrapAccountPasswordFragment +import im.vector.app.features.crypto.recover.BootstrapReAuthFragment import im.vector.app.features.crypto.recover.BootstrapConclusionFragment import im.vector.app.features.crypto.recover.BootstrapConfirmPassphraseFragment import im.vector.app.features.crypto.recover.BootstrapEnterPassphraseFragment @@ -522,8 +522,8 @@ interface FragmentModule { @Binds @IntoMap - @FragmentKey(BootstrapAccountPasswordFragment::class) - fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment + @FragmentKey(BootstrapReAuthFragment::class) + fun bindBootstrapAccountPasswordFragment(fragment: BootstrapReAuthFragment): Fragment @Binds @IntoMap 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 index 6556e6ae65..917f60dacb 100644 --- a/vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt +++ b/vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt @@ -51,19 +51,23 @@ class PromptFragment : VectorBaseFragment() { } private fun onButtonClicked() = withState(viewModel) { state -> - if (state.flowType == LoginFlowTypes.SSO) { - viewModel.handle(ReAuthActions.StartSSOFallback) - } else if (state.flowType == 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)) + 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 } - } else { - // not supported } } @@ -91,5 +95,23 @@ class PromptFragment : VectorBaseFragment() { 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/features/auth/ReAuthActivity.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt index 0c27911e0e..cd7a32275d 100644 --- a/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt @@ -38,6 +38,7 @@ 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.internal.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.internal.auth.registration.nextUncompletedStage import timber.log.Timber import javax.inject.Inject @@ -47,7 +48,8 @@ class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory { data class Args( val flowType: String?, val title: String?, - val session: String? + val session: String?, + val lastErrorCode: String? ) : Parcelable // For sso @@ -196,17 +198,21 @@ class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory { const val RESULT_FLOW_TYPE = "RESULT_FLOW_TYPE" const val RESULT_VALUE = "RESULT_VALUE" - fun newIntent(context: Context, fromError: RegistrationFlowResponse, reasonTitle: String?): Intent { - val authType = if (fromError.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true }) { - LoginFlowTypes.PASSWORD - } else if (fromError.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.SSO) == true }) { - LoginFlowTypes.SSO - } else { - // TODO, support more auth type? - null + fun newIntent(context: Context, fromError: RegistrationFlowResponse, lastErrorCode: String?, reasonTitle: String?): 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)) + putExtra(MvRx.KEY_ARG, Args(authType, reasonTitle, fromError.session, lastErrorCode)) } } } 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 index f80c9acdd2..633743dbcb 100644 --- a/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt @@ -23,12 +23,14 @@ data class ReAuthState( val session: String? = null, val flowType: String? = null, val ssoFallbackPageWasShown: Boolean = false, - val passwordVisible: Boolean = false + val passwordVisible: Boolean = false, + val lastErrorCode: String? = null ) : MvRxState { constructor(args: ReAuthActivity.Args) : this( args.title, args.session, - args.flowType + args.flowType, + lastErrorCode = args.lastErrorCode ) constructor() : this(null, null) 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 149bd629e1..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,6 +16,7 @@ package im.vector.app.features.crypto.recover +import android.app.Activity import android.app.Dialog import android.os.Build import android.os.Bundle @@ -36,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 @@ -64,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 -> @@ -85,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) + } + } } } } @@ -149,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 ebc8239765..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 @@ -21,10 +21,8 @@ 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.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.auth.data.LoginFlowTypes 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 @@ -34,21 +32,15 @@ import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec -import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo 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.UIABaseAuth -import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth import org.matrix.android.sdk.internal.util.awaitCallback import timber.log.Timber -import java.lang.UnsupportedOperationException import java.util.UUID import javax.inject.Inject -import kotlin.coroutines.Continuation -import kotlin.coroutines.resume sealed class BootstrapResult { @@ -57,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 { @@ -74,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, @@ -107,21 +95,10 @@ class BootstrapCrossSigningTask @Inject constructor( try { awaitCallback { - crossSigningService.initializeCrossSigning(object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, promise: Continuation) { - if (flowResponse.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true) { - val updatedAuth = params.userPasswordAuth?.copy(session = flowResponse.session) - if (updatedAuth == null) { - promise.resumeWith(Result.failure(UnsupportedOperationException())) - } else { - promise.resume(updatedAuth) - } - } else { - promise.resumeWith(Result.failure(UnsupportedOperationException())) - } - } - }, - it) + crossSigningService.initializeCrossSigning( + params.userInteractiveAuthInterceptor, + it + ) } if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) { return BootstrapResult.SuccessCrossSigningOnly @@ -332,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..7080cdd9c3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthFragment.kt @@ -0,0 +1,86 @@ +/* + * 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.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 + if (failure == null) { + views.reAuthFailureText.text = null + views.reAuthFailureText.isVisible = false + views.waitingProgress.isVisible = true + views.bootstrapCancelButton.isVisible = false + views.bootstrapRetryButton.isVisible = false + } else { + views.reAuthFailureText.text = failure + views.reAuthFailureText.isVisible = true + 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 3a6f57198e..d67c9afd42 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 @@ -26,8 +26,8 @@ import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import com.nulabinc.zxcvbn.Zxcvbn import dagger.assisted.Assisted -import dagger.assisted.AssistedInject 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 @@ -37,14 +37,22 @@ import im.vector.app.core.resources.StringProvider 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.internal.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.internal.auth.registration.nextUncompletedStage 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.DefaultBaseAuth +import org.matrix.android.sdk.internal.crypto.model.rest.UIABaseAuth import org.matrix.android.sdk.internal.crypto.model.rest.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, @@ -66,14 +74,17 @@ class BootstrapSharedViewModel @AssistedInject constructor( 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 { when (args.setUpMode) { SetupMode.PASSPHRASE_RESET, SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET, - SetupMode.HARD_RESET -> { + SetupMode.HARD_RESET -> { setState { copy(step = BootstrapStep.FirstForm(keyBackUpExist = false, reset = true)) } @@ -81,10 +92,10 @@ 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 -> { + SetupMode.NORMAL -> { // need to check if user have an existing keybackup setState { copy(step = BootstrapStep.CheckingMigration) @@ -136,8 +147,8 @@ class BootstrapSharedViewModel @AssistedInject constructor( override fun handle(action: BootstrapActions) = withState { state -> when (action) { - is BootstrapActions.GoBack -> queryBack() - BootstrapActions.TogglePasswordVisibility -> { + is BootstrapActions.GoBack -> queryBack() + BootstrapActions.TogglePasswordVisibility -> { when (state.step) { is BootstrapStep.SetupPassphrase -> { setState { @@ -149,10 +160,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 { @@ -162,13 +171,13 @@ class BootstrapSharedViewModel @AssistedInject constructor( else -> Unit } } - BootstrapActions.StartKeyBackupMigration -> { + BootstrapActions.StartKeyBackupMigration -> { handleStartMigratingKeyBackup() } - is BootstrapActions.Start -> { + is BootstrapActions.Start -> { handleStart(action) } - is BootstrapActions.UpdateCandidatePassphrase -> { + is BootstrapActions.UpdateCandidatePassphrase -> { val strength = zxcvbn.measure(action.pass) setState { copy( @@ -177,7 +186,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( ) } } - is BootstrapActions.GoToConfirmPassphrase -> { + is BootstrapActions.GoToConfirmPassphrase -> { setState { copy( passphrase = action.passphrase, @@ -194,18 +203,9 @@ class BootstrapSharedViewModel @AssistedInject constructor( ) } } - is BootstrapActions.DoInitialize -> { + 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,74 +214,74 @@ 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) - } + is BootstrapActions.DoInitializeGeneratedKey -> { + startInitializeFlow(state) } - BootstrapActions.RecoveryKeySaved -> { + BootstrapActions.RecoveryKeySaved -> { _viewEvents.post(BootstrapViewEvents.RecoveryKeySaved) setState { copy(step = BootstrapStep.SaveRecoveryKey(true)) } } - BootstrapActions.Completed -> { + BootstrapActions.Completed -> { _viewEvents.post(BootstrapViewEvents.Dismiss(true)) } - BootstrapActions.GoToCompleted -> { + BootstrapActions.GoToCompleted -> { setState { copy(step = BootstrapStep.DoneSuccess) } } - BootstrapActions.SaveReqQueryStarted -> { + BootstrapActions.SaveReqQueryStarted -> { setState { copy(recoverySaveFileProcess = Loading()) } } - is BootstrapActions.SaveKeyToUri -> { + is BootstrapActions.SaveKeyToUri -> { saveRecoveryKeyToUri(action.os) } - BootstrapActions.SaveReqFailed -> { + BootstrapActions.SaveReqFailed -> { setState { copy(recoverySaveFileProcess = Uninitialized) } } - BootstrapActions.GoToEnterAccountPassword -> { + BootstrapActions.GoToEnterAccountPassword -> { setState { - copy(step = BootstrapStep.AccountPassword(false)) + copy(step = BootstrapStep.AccountReAuth()) } } - BootstrapActions.HandleForgotBackupPassphrase -> { + BootstrapActions.HandleForgotBackupPassphrase -> { if (state.step is BootstrapStep.GetBackupSecretPassForMigration) { setState { copy(step = BootstrapStep.GetBackupSecretPassForMigration(state.step.isPasswordVisible, true)) } } else return@withState } - is BootstrapActions.ReAuth -> { - startInitializeFlow(action.pass) - } - is BootstrapActions.DoMigrateWithPassphrase -> { +// is BootstrapActions.ReAuth -> { +// startInitializeFlow(action.pass) +// } + is BootstrapActions.DoMigrateWithPassphrase -> { startMigrationFlow(state.step, action.passphrase, null) } - is BootstrapActions.DoMigrateWithRecoveryKey -> { + is BootstrapActions.DoMigrateWithRecoveryKey -> { startMigrationFlow(state.step, null, action.recoveryKey) } + BootstrapActions.SsoAuthDone -> { + uiaContinuation?.resume(DefaultBaseAuth(session = pendingAuth?.session ?: "")) + } + is BootstrapActions.PasswordAuthDone -> { + uiaContinuation?.resume( + UserPasswordAuth( + session = pendingAuth?.session, + password = action.password, + user = session.myUserId + ) + ) + } + BootstrapActions.ReAuthCancelled -> { + setState { + copy(step = BootstrapStep.AccountReAuth(stringProvider.getString(R.string.authentication_error))) + } + } }.exhaustive } @@ -293,7 +293,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( ) } } else { - startInitializeFlow(null) + startInitializeFlow(it) } } @@ -346,16 +346,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( @@ -372,7 +372,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( } } - private fun startInitializeFlow(userPassword: String?) = withState { state -> + private fun startInitializeFlow(state: BootstrapViewState) { val previousStep = state.step setState { @@ -389,19 +389,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) } }, @@ -410,10 +436,9 @@ class BootstrapSharedViewModel @AssistedInject constructor( ) { bootstrapResult -> when (bootstrapResult) { is BootstrapResult.SuccessCrossSigningOnly -> { - // TPD _viewEvents.post(BootstrapViewEvents.Dismiss(true)) } - is BootstrapResult.Success -> { + is BootstrapResult.Success -> { setState { copy( recoveryKeyCreationInfo = bootstrapResult.keyInfo, @@ -424,30 +449,15 @@ class BootstrapSharedViewModel @AssistedInject constructor( ) } } - is BootstrapResult.PasswordAuthFlowMissing -> { - // Ask the password to the user - _pendingSession = bootstrapResult.sessionId + is BootstrapResult.InvalidPasswordError -> { + // it's a bad password / auth setState { copy( - step = BootstrapStep.AccountPassword(false) + step = BootstrapStep.AccountReAuth(stringProvider.getString(R.string.auth_invalid_login_param)) ) } } - 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 - setState { - copy( - step = BootstrapStep.AccountPassword(false, stringProvider.getString(R.string.auth_invalid_login_param)) - ) - } - } - is BootstrapResult.Failure -> { + is BootstrapResult.Failure -> { if (bootstrapResult is BootstrapResult.GenericError && bootstrapResult.failure is Failure.OtherServerError && bootstrapResult.failure.httpCode == 401) { @@ -497,7 +507,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( } } } - is BootstrapStep.SetupPassphrase -> { + is BootstrapStep.SetupPassphrase -> { setState { copy( step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist), @@ -507,7 +517,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( ) } } - is BootstrapStep.ConfirmPassphrase -> { + is BootstrapStep.ConfirmPassphrase -> { setState { copy( step = BootstrapStep.SetupPassphrase( @@ -516,19 +526,19 @@ class BootstrapSharedViewModel @AssistedInject constructor( ) } } - is BootstrapStep.AccountPassword -> { + is BootstrapStep.AccountReAuth -> { _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null)) } - BootstrapStep.Initializing -> { + BootstrapStep.Initializing -> { // do we let you cancel from here? _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null)) } is BootstrapStep.SaveRecoveryKey, - BootstrapStep.DoneSuccess -> { + BootstrapStep.DoneSuccess -> { // nop } - BootstrapStep.CheckingMigration -> Unit - is BootstrapStep.FirstForm -> { + BootstrapStep.CheckingMigration -> Unit + is BootstrapStep.FirstForm -> { _viewEvents.post( when (args.setUpMode) { SetupMode.CROSS_SIGNING_ONLY, @@ -537,7 +547,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( } ) } - is BootstrapStep.GetBackupSecretForMigration -> { + is BootstrapStep.GetBackupSecretForMigration -> { setState { copy( step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist), @@ -555,7 +565,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( private fun BackupToQuadSMigrationTask.Result.Failure.toHumanReadable(): String { return when (this) { is BackupToQuadSMigrationTask.Result.InvalidRecoverySecret -> stringProvider.getString(R.string.keys_backup_passphrase_error_decrypt) - is BackupToQuadSMigrationTask.Result.ErrorFailure -> errorFormatter.toHumanReadable(throwable) + is BackupToQuadSMigrationTask.Result.ErrorFailure -> errorFormatter.toHumanReadable(throwable) // is BackupToQuadSMigrationTask.Result.NoKeyBackupVersion, // is BackupToQuadSMigrationTask.Result.IllegalParams, else -> stringProvider.getString(R.string.unexpected_error) 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..f4ec9d68e4 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.internal.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/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 5ba617b379..e19205560c 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 @@ -41,6 +41,7 @@ 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.internal.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.internal.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.UIABaseAuth @@ -159,8 +160,8 @@ class HomeActivityViewModel @AssistedInject constructor( Timber.d("Initialize cross signing") session.cryptoService().crossSigningService().initializeCrossSigning( object : UserInteractiveAuthInterceptor { - override fun performStage(flow: RegistrationFlowResponse, promise: Continuation) { - if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true) { + override fun performStage(flow: RegistrationFlowResponse, errorCode: String?, promise: Continuation) { + if (flow.nextUncompletedStage() == LoginFlowTypes.PASSWORD) { promise.resume( UserPasswordAuth( session = flow.session, @@ -251,8 +252,8 @@ class HomeActivityViewModel @AssistedInject constructor( Timber.d("Initialize cross signing") session.cryptoService().crossSigningService().initializeCrossSigning( object : UserInteractiveAuthInterceptor { - override fun performStage(flow: RegistrationFlowResponse, promise: Continuation) { - if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true) { + override fun performStage(flow: RegistrationFlowResponse, errorCode: String?, promise: Continuation) { + if (flow.nextUncompletedStage() == LoginFlowTypes.PASSWORD) { UserPasswordAuth( session = flow.session, user = session.myUserId, 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/settings/crosssigning/CrossSigningSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt index 4625c21cdd..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 @@ -88,7 +88,10 @@ class CrossSigningSettingsFragment @Inject constructor( Unit } is CrossSigningSettingsViewEvents.RequestReAuth -> { - ReAuthActivity.newIntent(requireContext(), event.registrationFlowResponse, getString(R.string.initialize_cross_signing)).let { intent -> + ReAuthActivity.newIntent(requireContext(), + event.registrationFlowResponse, + event.lastErrorCode, + getString(R.string.initialize_cross_signing)).let { intent -> reAuthActivityResultLauncher.launch(intent) } } 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 8da5da4c7b..89970b130a 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 @@ -24,7 +24,7 @@ import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowRespons */ sealed class CrossSigningSettingsViewEvents : VectorViewEvents { data class Failure(val throwable: Throwable) : CrossSigningSettingsViewEvents() - data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse) : 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 06fa930ef1..9a1150046d 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 @@ -37,6 +37,7 @@ 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.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.internal.auth.registration.nextUncompletedStage 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 @@ -95,9 +96,9 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( awaitCallback { session.cryptoService().crossSigningService().initializeCrossSigning( object : UserInteractiveAuthInterceptor { - override fun performStage(flow: RegistrationFlowResponse, promise: Continuation) { + override fun performStage(flow: RegistrationFlowResponse, errorCode: String?, promise: Continuation) { Timber.d("## UIA : initializeCrossSigning UIA") - if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true && reAuthHelper.data != null) { + if (flow.nextUncompletedStage() == LoginFlowTypes.PASSWORD && reAuthHelper.data != null && errorCode == null) { UserPasswordAuth( session = null, user = session.myUserId, @@ -105,7 +106,7 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( ).let { promise.resume(it) } } else { Timber.d("## UIA : initializeCrossSigning UIA > start reauth activity") - _viewEvents.post(CrossSigningSettingsViewEvents.RequestReAuth(flow)) + _viewEvents.post(CrossSigningSettingsViewEvents.RequestReAuth(flow, errorCode)) pendingAuth = DefaultBaseAuth(session = flow.session) uiaContinuation = promise } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewState.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewState.kt index aef27c598e..8a371ada68 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewState.kt @@ -23,6 +23,5 @@ data class CrossSigningSettingsViewState( val crossSigningInfo: MXCrossSigningInfo? = null, val xSigningIsEnableInAccount: Boolean = false, val xSigningKeysAreTrusted: Boolean = false, - val xSigningKeyCanSign: Boolean = true, -// val pendingAuthSession: String? = null + val xSigningKeyCanSign: Boolean = true ) : MvRxState 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 5644d1c63e..fd50fe6f16 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 @@ -33,7 +33,7 @@ sealed class DevicesViewEvents : VectorViewEvents { // object RequestPassword : DevicesViewEvents() - data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse) : 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 95b1619b21..db29ced873 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 @@ -49,6 +49,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic 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.auth.registration.RegistrationFlowResponse +import org.matrix.android.sdk.internal.auth.registration.nextUncompletedStage import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth @@ -330,9 +331,9 @@ class DevicesViewModel @AssistedInject constructor( try { awaitCallback { session.cryptoService().deleteDevice(deviceId, object : UserInteractiveAuthInterceptor { - override fun performStage(flow: RegistrationFlowResponse, promise: Continuation) { + override fun performStage(flow: RegistrationFlowResponse, errorCode: String?, promise: Continuation) { Timber.d("## UIA : deleteDevice UIA") - if (flow.flows?.any { it.type == LoginFlowTypes.PASSWORD } == true && reAuthHelper.data != null) { + if (flow.nextUncompletedStage() == LoginFlowTypes.PASSWORD && reAuthHelper.data != null && errorCode == null) { UserPasswordAuth( session = null, user = session.myUserId, @@ -340,7 +341,7 @@ class DevicesViewModel @AssistedInject constructor( ).let { promise.resume(it) } } else { Timber.d("## UIA : deleteDevice UIA > start reauth activity") - _viewEvents.post(DevicesViewEvents.RequestReAuth(flow)) + _viewEvents.post(DevicesViewEvents.RequestReAuth(flow, errorCode)) pendingAuth = DefaultBaseAuth(session = flow.session) uiaContinuation = promise } 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 e4bc109d58..d1b488fadf 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 @@ -173,7 +173,10 @@ class VectorSettingsDevicesFragment @Inject constructor( * Show a dialog to ask for user password, or use a previously entered password. */ private fun maybeShowDeleteDeviceWithPasswordDialog(reAuthReq: DevicesViewEvents.RequestReAuth) { - ReAuthActivity.newIntent(requireContext(), reAuthReq.registrationFlowResponse, getString(R.string.devices_delete_dialog_title)).let { intent -> + 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/workers/signout/ServerBackupStatusViewModel.kt b/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt index a73facc009..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 @@ -115,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/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_reauth_confirm.xml b/vector/src/main/res/layout/fragment_reauth_confirm.xml index b8f1a57ae3..e23c7c9249 100644 --- a/vector/src/main/res/layout/fragment_reauth_confirm.xml +++ b/vector/src/main/res/layout/fragment_reauth_confirm.xml @@ -69,14 +69,28 @@ + + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/genericErrorText" /> \ No newline at end of file From 9c7df258622fdc653e20687ebbae088150cb6854 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 1 Feb 2021 14:52:11 +0100 Subject: [PATCH 31/51] Relax rule for e2e by default --- .../internal/session/room/create/CreateRoomBodyBuilder.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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..48b578a519 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) From 5b8215a356c190739db01044af585dc31071732d Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 1 Feb 2021 16:36:38 +0100 Subject: [PATCH 32/51] Support SSO provider brand + UI fixes --- .../sdk/api/auth/data/SsoIdentityProvider.kt | 23 ++++++--- .../internal/auth/data/LoginFlowResponse.kt | 10 +++- .../features/login/SocialLoginButtonsView.kt | 30 +++++++----- .../main/res/drawable/ic_social_gitlab.xml | 48 +++++++++++++++++++ vector/src/main/res/layout/fragment_login.xml | 1 - ...fragment_login_signup_signin_selection.xml | 1 - vector/src/main/res/values/attrs.xml | 1 + .../main/res/values/styles_social_login.xml | 26 ++++++++-- vector/src/main/res/values/theme_dark.xml | 1 + vector/src/main/res/values/theme_light.xml | 1 + 10 files changed, 115 insertions(+), 27 deletions(-) create mode 100644 vector/src/main/res/drawable/ic_social_gitlab.xml 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/data/LoginFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt index 2b26115f30..112f7a1078 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,11 @@ internal data class LoginFlow( * See MSC #2858 */ @Json(name = "org.matrix.msc2858.identity_providers") - val ssoIdentityProvider: List? -) + val _devSsoIdentityProvider: List? = null, + + @Json(name = "identity_providers") + val _ssoIdentityProvider: List? = null, + +) { + val ssoIdentityProvider = _ssoIdentityProvider ?: _devSsoIdentityProvider +} 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..cffe44d0f0 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/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/fragment_login.xml b/vector/src/main/res/layout/fragment_login.xml index da41878365..1740d26b3b 100644 --- a/vector/src/main/res/layout/fragment_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -143,7 +143,6 @@ android:layout_height="wrap_content" android:gravity="center" android:orientation="vertical" - android:padding="8dp" android:visibility="gone" tools:visibility="visible"> diff --git a/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml b/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml index 097e5c1f52..56d4e37f1e 100644 --- a/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml +++ b/vector/src/main/res/layout/fragment_login_signup_signin_selection.xml @@ -88,7 +88,6 @@ android:layout_height="wrap_content" android:gravity="center" android:orientation="vertical" - android:padding="8dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/vector/src/main/res/values/attrs.xml b/vector/src/main/res/values/attrs.xml index 51d140ebcf..41b8080fc0 100644 --- a/vector/src/main/res/values/attrs.xml +++ b/vector/src/main/res/values/attrs.xml @@ -46,6 +46,7 @@ + diff --git a/vector/src/main/res/values/styles_social_login.xml b/vector/src/main/res/values/styles_social_login.xml index 796965cee1..3ad7fbb989 100644 --- a/vector/src/main/res/values/styles_social_login.xml +++ b/vector/src/main/res/values/styles_social_login.xml @@ -1,17 +1,20 @@ - + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index 86fbb57608..c31fc8240b 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -200,6 +200,7 @@ @style/WidgetButtonSocialLogin.Facebook.Dark @style/WidgetButtonSocialLogin.Twitter.Dark @style/WidgetButtonSocialLogin.Apple.Dark + @style/WidgetButtonSocialLogin.Gitlab.Dark @android:color/transparent diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml index f174bcf758..56faaeb325 100644 --- a/vector/src/main/res/values/theme_light.xml +++ b/vector/src/main/res/values/theme_light.xml @@ -202,6 +202,7 @@ @style/WidgetButtonSocialLogin.Facebook.Light @style/WidgetButtonSocialLogin.Twitter.Light @style/WidgetButtonSocialLogin.Apple.Light + @style/WidgetButtonSocialLogin.Gitlab.Light @color/black_alpha From 76b425ee8a75d5b14debdd039181345967acab50 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 1 Feb 2021 16:38:20 +0100 Subject: [PATCH 33/51] Cleaning --- .../android/sdk/internal/auth/data/LoginFlowResponse.kt | 2 +- .../sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt | 4 ++-- .../im/vector/app/features/login/SocialLoginButtonsView.kt | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) 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 112f7a1078..703858fb61 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 @@ -46,7 +46,7 @@ internal data class LoginFlow( val _devSsoIdentityProvider: List? = null, @Json(name = "identity_providers") - val _ssoIdentityProvider: List? = null, + val _ssoIdentityProvider: List? = null ) { val ssoIdentityProvider = _ssoIdentityProvider ?: _devSsoIdentityProvider 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 5856d705a8..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 @@ -125,8 +125,8 @@ internal class DefaultInitializeCrossSigningTask @Inject constructor( try { uploadSigningKeysTask.execute(uploadSigningKeysParams) } catch (failure: Throwable) { - if (params.interactiveAuthInterceptor == null || - !handleUIA(failure, params.interactiveAuthInterceptor) { authUpdate -> + if (params.interactiveAuthInterceptor == null + || !handleUIA(failure, params.interactiveAuthInterceptor) { authUpdate -> uploadSigningKeysTask.execute(uploadSigningKeysParams.copy(userAuthParam = authUpdate)) }) { Timber.d("## UIA: propagate failure") 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 cffe44d0f0..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 @@ -135,9 +135,9 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: if (isInEditMode) { ssoIdentityProviders = listOf( 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("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) From 6c9b16088fbd2066d54d69b7ddcc7ce9fc9a99e3 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 2 Feb 2021 08:59:28 +0100 Subject: [PATCH 34/51] Secure secrets passed to intent --- .../im/vector/app/features/auth/ReAuthActivity.kt | 14 ++++++++++---- .../im/vector/app/features/auth/ReAuthEvents.kt | 2 +- .../im/vector/app/features/auth/ReAuthState.kt | 6 ++++-- .../im/vector/app/features/auth/ReAuthViewModel.kt | 13 +++++++++---- .../crypto/recover/BootstrapSharedViewModel.kt | 5 ++++- .../crosssigning/CrossSigningSettingsViewModel.kt | 5 ++++- .../features/settings/devices/DevicesViewModel.kt | 5 ++++- 7 files changed, 36 insertions(+), 14 deletions(-) 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 index cd7a32275d..a95c2b73cc 100644 --- a/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt @@ -49,7 +49,8 @@ class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory { val flowType: String?, val title: String?, val session: String?, - val lastErrorCode: String? + val lastErrorCode: String?, + val resultKeyStoreAlias: String ) : Parcelable // For sso @@ -101,7 +102,7 @@ class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory { is ReAuthEvents.PasswordFinishSuccess -> { setResult(RESULT_OK, Intent().apply { putExtra(RESULT_FLOW_TYPE, LoginFlowTypes.PASSWORD) - putExtra(RESULT_VALUE, it.password) + putExtra(RESULT_VALUE, it.passwordSafeForIntent) }) finish() } @@ -197,8 +198,13 @@ class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory { 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?): Intent { + 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 @@ -212,7 +218,7 @@ class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory { } } return Intent(context, ReAuthActivity::class.java).apply { - putExtra(MvRx.KEY_ARG, Args(authType, reasonTitle, fromError.session, lastErrorCode)) + putExtra(MvRx.KEY_ARG, Args(authType, reasonTitle, fromError.session, lastErrorCode, resultKeyStoreAlias)) } } } diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthEvents.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthEvents.kt index 303d87a4c7..8cf9be6fb1 100644 --- a/vector/src/main/java/im/vector/app/features/auth/ReAuthEvents.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthEvents.kt @@ -21,5 +21,5 @@ 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 password: String) : 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 index 633743dbcb..540a08405c 100644 --- a/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt @@ -24,13 +24,15 @@ data class ReAuthState( val flowType: String? = null, val ssoFallbackPageWasShown: Boolean = false, val passwordVisible: Boolean = false, - val lastErrorCode: String? = null + val lastErrorCode: String? = null, + val resultKeyStoreAlias: String = "" ) : MvRxState { constructor(args: ReAuthActivity.Args) : this( args.title, args.session, args.flowType, - lastErrorCode = args.lastErrorCode + 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 index d29bf2828d..4b477990c0 100644 --- a/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt @@ -24,14 +24,14 @@ 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.AuthenticationService 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, - private val authenticationService: AuthenticationService + private val session: Session ) : VectorViewModel(initialState) { @AssistedFactory @@ -72,7 +72,12 @@ class ReAuthViewModel @AssistedInject constructor( } } is ReAuthActions.ReAuthWithPass -> { - _viewEvents.post(ReAuthEvents.PasswordFinishSuccess(action.password)) + 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/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt index d67c9afd42..9282dc3e3e 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 @@ -34,6 +34,7 @@ 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 @@ -44,6 +45,7 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.internal.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.DefaultBaseAuth @@ -269,10 +271,11 @@ class BootstrapSharedViewModel @AssistedInject constructor( 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 = action.password, + password = decryptedPass, user = session.myUserId ) ) 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 9a1150046d..d29ecefff1 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 @@ -26,6 +26,7 @@ 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 @@ -38,6 +39,7 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.internal.auth.registration.nextUncompletedStage +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 @@ -134,10 +136,11 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( Unit } is CrossSigningSettingsAction.PasswordAuthDone -> { + val decryptedPass = session.loadSecureSecret(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS) uiaContinuation?.resume( UserPasswordAuth( session = pendingAuth?.session, - password = action.password, + password = decryptedPass, user = session.myUserId ) ) 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 db29ced873..610c89706e 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 @@ -32,6 +32,7 @@ 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 @@ -51,6 +52,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxStat import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.internal.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 @@ -221,10 +223,11 @@ class DevicesViewModel @AssistedInject constructor( Unit } is DevicesAction.PasswordAuthDone -> { + val decryptedPass = session.loadSecureSecret(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS) uiaContinuation?.resume( UserPasswordAuth( session = pendingAuth?.session, - password = action.password, + password = decryptedPass, user = session.myUserId ) ) From 0bc203e0d5306beefb6e8098c2b0a46447c9e56f Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 2 Feb 2021 09:02:10 +0100 Subject: [PATCH 35/51] Setup cross signing after initial sync if not yet done Use grace period if available, if not fail silently --- .../features/home/HomeActivityViewModel.kt | 131 +++++++----------- 1 file changed, 52 insertions(+), 79 deletions(-) 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 e19205560c..f8e61b5e04 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 @@ -29,12 +29,12 @@ 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 @@ -46,6 +46,7 @@ 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.UIABaseAuth import org.matrix.android.sdk.internal.crypto.model.rest.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 @@ -81,7 +82,6 @@ class HomeActivityViewModel @AssistedInject constructor( init { cleanupFiles() observeInitialSync() - mayBeInitializeCrossSigning() checkSessionPushIsOn() observeCrossSigningReset() } @@ -132,7 +132,7 @@ class HomeActivityViewModel @AssistedInject constructor( is InitialSyncProgressService.Status.Idle -> { if (checkBootstrap) { checkBootstrap = false - maybeBootstrapCrossSigning() + maybeBootstrapCrossSigningAfterInitialSync() } } } @@ -146,37 +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( - object : UserInteractiveAuthInterceptor { - override fun performStage(flow: RegistrationFlowResponse, errorCode: String?, promise: Continuation) { - if (flow.nextUncompletedStage() == LoginFlowTypes.PASSWORD) { - promise.resume( - UserPasswordAuth( - session = flow.session, - user = session.myUserId, - password = password - ) - ) - } else promise.resumeWith(Result.failure(UnsupportedOperationException())) - } - }, - callback = NoOpMatrixCallback() - ) - } - } - /** * After migration from riot to element some users reported that their * push setting for the session was set to off @@ -212,62 +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( object : UserInteractiveAuthInterceptor { override fun performStage(flow: RegistrationFlowResponse, errorCode: String?, promise: Continuation) { - if (flow.nextUncompletedStage() == LoginFlowTypes.PASSWORD) { - UserPasswordAuth( - session = flow.session, - user = session.myUserId, - password = password + // We missed server grace period or it's not setup, see if we remember locally password + if (flow.nextUncompletedStage() == LoginFlowTypes.PASSWORD + && errorCode == null + && reAuthHelper.data != null) { + promise.resume( + UserPasswordAuth( + session = flow.session, + user = session.myUserId, + password = reAuthHelper.data + ) ) - } else null + } else { + promise.resumeWith(Result.failure(Exception("Cannot silently initialize cross signing, UIA missing"))) + } } }, - callback = NoOpMatrixCallback() + callback = it ) + Timber.d("Initialize cross signing SUCCESS") + } catch (failure: Throwable) { + Timber.e(failure, "Failed to initialize cross signing") } } } - }) + } } override fun handle(action: HomeActivityViewActions) { From 8129cd0cd352a4908f4e49558e596888494e7a2d Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 2 Feb 2021 09:32:04 +0100 Subject: [PATCH 36/51] Cleaning + changelog --- CHANGES.md | 6 ++++-- .../android/sdk/api/auth/UserInteractiveAuthInterceptor.kt | 2 +- vector/src/main/AndroidManifest.xml | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 537e3aef7b..3d6c82a776 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,10 +2,12 @@ Changes in Element 1.0.15 (2020-XX-XX) =================================================== 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. 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 index 5c076b3f4b..e7f27f458c 100644 --- 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 @@ -41,7 +41,7 @@ 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 provider using promise.resume, this allow implementation to perform + * 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/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 1028f200af..43ad1d3182 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -248,7 +248,7 @@ android:exported="false"> From e6fc605b08e17be65247c57f3884fc594fcb7f08 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 2 Feb 2021 11:38:23 +0100 Subject: [PATCH 37/51] Data for Worker overload (#2721) --- CHANGES.md | 1 + .../DefaultCrossSigningService.kt | 24 ++++--- .../crypto/crosssigning/UpdateTrustWorker.kt | 25 ++++++- .../UpdateTrustWorkerDataRepository.kt | 65 +++++++++++++++++++ .../room/summary/RoomSummaryUpdater.kt | 3 +- 5 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorkerDataRepository.kt diff --git a/CHANGES.md b/CHANGES.md index 537e3aef7b..0f72e04574 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,7 @@ Bugfix 🐛: - 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) Translations 🗣: - 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..8c640a2719 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.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 @@ -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..af5255c605 --- /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 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 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/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) } } } From 2a3962265b6c1b05b1397ba598ab1725af93afa7 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 2 Feb 2021 12:30:46 +0100 Subject: [PATCH 38/51] SSO UIA for deactivate account --- .../android/sdk/api/failure/Extensions.kt | 6 ++ .../sdk/api/session/account/AccountService.kt | 4 +- .../account/DeactivateAccountParams.kt | 14 ++-- .../session/account/DeactivateAccountTask.kt | 25 +++++-- .../session/account/DefaultAccountService.kt | 5 +- .../app/features/auth/ReAuthActivity.kt | 2 +- .../app/features/auth/ReAuthViewModel.kt | 1 + .../deactivation/DeactivateAccountAction.kt | 28 +++++++ .../deactivation/DeactivateAccountFragment.kt | 73 ++++++++++--------- .../DeactivateAccountViewEvents.kt | 5 +- .../DeactivateAccountViewModel.kt | 68 ++++++++++++----- .../CrossSigningSettingsViewModel.kt | 6 +- .../layout/fragment_deactivate_account.xml | 65 +---------------- 13 files changed, 164 insertions(+), 138 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountAction.kt 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 8d8df6ff82..a0983161af 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 @@ -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 */ 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/internal/session/account/DeactivateAccountParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountParams.kt index 6c2e8b4a4e..a99a589ec4 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.internal.crypto.model.rest.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..148afa7c90 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.internal.crypto.model.rest.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/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt index a95c2b73cc..b44639750e 100644 --- a/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt @@ -154,7 +154,7 @@ class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory { Timber.v("## CustomTab onNavigationEvent($navigationEvent), $extras") super.onNavigationEvent(navigationEvent, extras) if (navigationEvent == NAVIGATION_FINISHED) { - sharedViewModel.handle(ReAuthActions.FallBackPageLoaded) +// sharedViewModel.handle(ReAuthActions.FallBackPageLoaded) } } 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 index 4b477990c0..a946a91ced 100644 --- a/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt @@ -54,6 +54,7 @@ class ReAuthViewModel @AssistedInject constructor( when (action) { ReAuthActions.StartSSOFallback -> { if (state.flowType == LoginFlowTypes.SSO) { + setState { copy(ssoFallbackPageWasShown = true) } val ssoURL = session.getUIASsoFallbackUrl(initialState.session ?: "") _viewEvents.post(ReAuthEvents.OpenSsoURl(ssoURL)) } 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..05200c3aa3 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.internal.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 6a7084fb81..dc5415a6bb 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 @@ -21,25 +21,28 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject 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.internal.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.internal.crypto.model.rest.UIABaseAuth +import org.matrix.android.sdk.internal.crypto.model.rest.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) { @@ -49,10 +52,37 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v 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 } @@ -63,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/CrossSigningSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt index d29ecefff1..ceb216ca42 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 @@ -124,16 +124,12 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( Unit } is CrossSigningSettingsAction.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") + Timber.d("## UIA - FallBack success") if (pendingAuth != null) { uiaContinuation?.resume(pendingAuth!!) } else { uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException()))) } - Unit } is CrossSigningSettingsAction.PasswordAuthDone -> { val decryptedPass = session.loadSecureSecret(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS) 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" /> - - - - - - - - - - - - - - -