From 04f0146afd193d34e43c8d0061e3ba5ad2b2ddb7 Mon Sep 17 00:00:00 2001 From: Tobias Preuss Date: Wed, 24 Jun 2020 12:16:30 +0200 Subject: [PATCH 01/41] Use Context#withStyledAttributes extension function. + This function is more concise and ensures "recycle()" is always invoked. + Sources: https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-preference-release/core/core-ktx/src/main/java/androidx/core/content/Context.kt#52 --- CHANGES.md | 2 +- .../behavior/PercentViewBehavior.kt | 29 +++++---- .../core/platform/EllipsizingTextView.kt | 8 ++- .../core/platform/MaxHeightScrollView.kt | 9 +-- .../core/ui/views/BottomSheetActionButton.kt | 18 ++--- .../timeline/item/PollResultLineView.kt | 13 ++-- .../reactions/widget/ReactionButton.kt | 65 ++++++++++--------- 7 files changed, 75 insertions(+), 69 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 20b3b34375..22220d3b0b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,7 +26,7 @@ Build 🧱: - Upgrade gradle from 5.4.1 to 5.6.4 Other changes: - - + - Use `Context#withStyledAttributes` extension function (#1546) Changes in Riot.imX 0.91.3 (2020-07-01) =================================================== diff --git a/vector/src/main/java/im/vector/riotx/core/animations/behavior/PercentViewBehavior.kt b/vector/src/main/java/im/vector/riotx/core/animations/behavior/PercentViewBehavior.kt index 967d7d638d..37c07b8293 100644 --- a/vector/src/main/java/im/vector/riotx/core/animations/behavior/PercentViewBehavior.kt +++ b/vector/src/main/java/im/vector/riotx/core/animations/behavior/PercentViewBehavior.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright 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. @@ -22,6 +22,7 @@ import android.graphics.drawable.ColorDrawable import android.util.AttributeSet import android.view.View import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.content.withStyledAttributes import im.vector.riotx.R import kotlin.math.abs @@ -67,19 +68,19 @@ class PercentViewBehavior(context: Context, attrs: AttributeSet) : Coo private var isPrepared: Boolean = false init { - val a = context.obtainStyledAttributes(attrs, R.styleable.PercentViewBehavior) - dependViewId = a.getResourceId(R.styleable.PercentViewBehavior_behavior_dependsOn, 0) - dependType = a.getInt(R.styleable.PercentViewBehavior_behavior_dependType, DEPEND_TYPE_WIDTH) - dependTarget = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_dependTarget, UNSPECIFIED_INT) - targetX = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetX, UNSPECIFIED_INT) - targetY = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetY, UNSPECIFIED_INT) - targetWidth = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetWidth, UNSPECIFIED_INT) - targetHeight = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetHeight, UNSPECIFIED_INT) - targetBackgroundColor = a.getColor(R.styleable.PercentViewBehavior_behavior_targetBackgroundColor, UNSPECIFIED_INT) - targetAlpha = a.getFloat(R.styleable.PercentViewBehavior_behavior_targetAlpha, UNSPECIFIED_FLOAT) - targetRotateX = a.getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateX, UNSPECIFIED_FLOAT) - targetRotateY = a.getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateY, UNSPECIFIED_FLOAT) - a.recycle() + context.withStyledAttributes(attrs, R.styleable.PercentViewBehavior) { + dependViewId = getResourceId(R.styleable.PercentViewBehavior_behavior_dependsOn, 0) + dependType = getInt(R.styleable.PercentViewBehavior_behavior_dependType, DEPEND_TYPE_WIDTH) + dependTarget = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_dependTarget, UNSPECIFIED_INT) + targetX = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetX, UNSPECIFIED_INT) + targetY = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetY, UNSPECIFIED_INT) + targetWidth = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetWidth, UNSPECIFIED_INT) + targetHeight = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetHeight, UNSPECIFIED_INT) + targetBackgroundColor = getColor(R.styleable.PercentViewBehavior_behavior_targetBackgroundColor, UNSPECIFIED_INT) + targetAlpha = getFloat(R.styleable.PercentViewBehavior_behavior_targetAlpha, UNSPECIFIED_FLOAT) + targetRotateX = getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateX, UNSPECIFIED_FLOAT) + targetRotateY = getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateY, UNSPECIFIED_FLOAT) + } } private fun prepare(parent: CoordinatorLayout, child: View, dependency: View) { diff --git a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt index f451308c36..f54776fc40 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/EllipsizingTextView.kt @@ -38,6 +38,7 @@ import android.text.TextUtils.substring import android.text.style.ForegroundColorSpan import android.util.AttributeSet import androidx.appcompat.widget.AppCompatTextView +import androidx.core.content.withStyledAttributes import timber.log.Timber import java.util.ArrayList import java.util.regex.Pattern @@ -71,6 +72,7 @@ class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: Att private var maxLines = 0 private var lineSpacingMult = 1.0f private var lineAddVertPad = 0.0f + /** * The end punctuation which will be removed when appending [.ELLIPSIS]. */ @@ -408,9 +410,9 @@ class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: Att } init { - val a = context.obtainStyledAttributes(attrs, intArrayOf(android.R.attr.maxLines, android.R.attr.ellipsize), defStyle, 0) - maxLines = a.getInt(0, Int.MAX_VALUE) - a.recycle() + context.withStyledAttributes(attrs, intArrayOf(android.R.attr.maxLines, android.R.attr.ellipsize), defStyle) { + maxLines = getInt(0, Int.MAX_VALUE) + } setEndPunctuationPattern(DEFAULT_END_PUNCTUATION) val currentTextColor = currentTextColor val ellipsizeColor = Color.argb(ELLIPSIZE_ALPHA, Color.red(currentTextColor), Color.green(currentTextColor), Color.blue(currentTextColor)) diff --git a/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt b/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt index b8587750a3..99c158252f 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/MaxHeightScrollView.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright 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. @@ -18,6 +18,7 @@ package im.vector.riotx.core.platform import android.content.Context import android.util.AttributeSet +import androidx.core.content.withStyledAttributes import androidx.core.widget.NestedScrollView import im.vector.riotx.R @@ -34,9 +35,9 @@ class MaxHeightScrollView @JvmOverloads constructor(context: Context, attrs: Att init { if (attrs != null) { - val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView) - maxHeight = styledAttrs.getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT) - styledAttrs.recycle() + context.withStyledAttributes(attrs, R.styleable.MaxHeightScrollView) { + maxHeight = getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT) + } } } diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt index d29982c9e4..455e856833 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt @@ -25,6 +25,7 @@ import android.view.View import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView +import androidx.core.content.withStyledAttributes import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible @@ -117,16 +118,15 @@ class BottomSheetActionButton @JvmOverloads constructor( inflate(context, R.layout.item_verification_action, this) ButterKnife.bind(this) - val typedArray = context.obtainStyledAttributes(attrs, R.styleable.BottomSheetActionButton, 0, 0) - title = typedArray.getString(R.styleable.BottomSheetActionButton_actionTitle) ?: "" - subTitle = typedArray.getString(R.styleable.BottomSheetActionButton_actionDescription) ?: "" - forceStartPadding = typedArray.getBoolean(R.styleable.BottomSheetActionButton_forceStartPadding, false) - leftIcon = typedArray.getDrawable(R.styleable.BottomSheetActionButton_leftIcon) + context.withStyledAttributes(attrs, R.styleable.BottomSheetActionButton) { + title = getString(R.styleable.BottomSheetActionButton_actionTitle) ?: "" + subTitle = getString(R.styleable.BottomSheetActionButton_actionDescription) ?: "" + forceStartPadding = getBoolean(R.styleable.BottomSheetActionButton_forceStartPadding, false) + leftIcon = getDrawable(R.styleable.BottomSheetActionButton_leftIcon) - rightIcon = typedArray.getDrawable(R.styleable.BottomSheetActionButton_rightIcon) + rightIcon = getDrawable(R.styleable.BottomSheetActionButton_rightIcon) - tint = typedArray.getColor(R.styleable.BottomSheetActionButton_tint, ThemeUtils.getColor(context, android.R.attr.textColor)) - - typedArray.recycle() + tint = getColor(R.styleable.BottomSheetActionButton_tint, ThemeUtils.getColor(context, android.R.attr.textColor)) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/PollResultLineView.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/PollResultLineView.kt index c52b863658..bee3ca6c5b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/PollResultLineView.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/PollResultLineView.kt @@ -22,6 +22,7 @@ import android.view.View import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView +import androidx.core.content.withStyledAttributes import butterknife.BindView import butterknife.ButterKnife import im.vector.riotx.R @@ -73,11 +74,11 @@ class PollResultLineView @JvmOverloads constructor( orientation = HORIZONTAL ButterKnife.bind(this) - val typedArray = context.obtainStyledAttributes(attrs, R.styleable.PollResultLineView, 0, 0) - label = typedArray.getString(R.styleable.PollResultLineView_optionName) ?: "" - percent = typedArray.getString(R.styleable.PollResultLineView_optionCount) ?: "" - optionSelected = typedArray.getBoolean(R.styleable.PollResultLineView_optionSelected, false) - isWinner = typedArray.getBoolean(R.styleable.PollResultLineView_optionIsWinner, false) - typedArray.recycle() + context.withStyledAttributes(attrs, R.styleable.PollResultLineView) { + label = getString(R.styleable.PollResultLineView_optionName) ?: "" + percent = getString(R.styleable.PollResultLineView_optionCount) ?: "" + optionSelected = getBoolean(R.styleable.PollResultLineView_optionSelected, false) + isWinner = getBoolean(R.styleable.PollResultLineView_optionIsWinner, false) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt b/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt index 811d399b23..ec5aba8ee5 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright 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. @@ -34,6 +34,7 @@ import android.widget.TextView import androidx.annotation.ColorInt import androidx.annotation.ColorRes import androidx.core.content.ContextCompat +import androidx.core.content.withStyledAttributes import im.vector.riotx.EmojiCompatWrapper import im.vector.riotx.R import im.vector.riotx.core.di.HasScreenInjector @@ -110,41 +111,41 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut // emojiView?.typeface = this.emojiTypeFace ?: Typeface.DEFAULT - val array = context.obtainStyledAttributes(attrs, R.styleable.ReactionButton, defStyleAttr, 0) + context.withStyledAttributes(attrs, R.styleable.ReactionButton, defStyleAttr) { - onDrawable = ContextCompat.getDrawable(context, R.drawable.rounded_rect_shape) - offDrawable = ContextCompat.getDrawable(context, R.drawable.rounded_rect_shape_off) + onDrawable = ContextCompat.getDrawable(context, R.drawable.rounded_rect_shape) + offDrawable = ContextCompat.getDrawable(context, R.drawable.rounded_rect_shape_off) - circleStartColor = array.getColor(R.styleable.ReactionButton_circle_start_color, 0) + circleStartColor = getColor(R.styleable.ReactionButton_circle_start_color, 0) - if (circleStartColor != 0) { - circleView.startColor = circleStartColor + if (circleStartColor != 0) { + circleView.startColor = circleStartColor + } + + circleEndColor = getColor(R.styleable.ReactionButton_circle_end_color, 0) + + if (circleEndColor != 0) { + circleView.endColor = circleEndColor + } + + dotPrimaryColor = getColor(R.styleable.ReactionButton_dots_primary_color, 0) + dotSecondaryColor = getColor(R.styleable.ReactionButton_dots_secondary_color, 0) + + if (dotPrimaryColor != 0 && dotSecondaryColor != 0) { + dotsView.setColors(dotPrimaryColor, dotSecondaryColor) + } + + getString(R.styleable.ReactionButton_emoji)?.let { + reactionString = it + } + + reactionCount = getInt(R.styleable.ReactionButton_reaction_count, 0) + + val status = getBoolean(R.styleable.ReactionButton_toggled, false) + setChecked(status) + setOnClickListener(this@ReactionButton) + setOnLongClickListener(this@ReactionButton) } - - circleEndColor = array.getColor(R.styleable.ReactionButton_circle_end_color, 0) - - if (circleEndColor != 0) { - circleView.endColor = circleEndColor - } - - dotPrimaryColor = array.getColor(R.styleable.ReactionButton_dots_primary_color, 0) - dotSecondaryColor = array.getColor(R.styleable.ReactionButton_dots_secondary_color, 0) - - if (dotPrimaryColor != 0 && dotSecondaryColor != 0) { - dotsView.setColors(dotPrimaryColor, dotSecondaryColor) - } - - array.getString(R.styleable.ReactionButton_emoji)?.let { - reactionString = it - } - - reactionCount = array.getInt(R.styleable.ReactionButton_reaction_count, 0) - - val status = array.getBoolean(R.styleable.ReactionButton_toggled, false) - setChecked(status) - setOnClickListener(this) - setOnLongClickListener(this) - array.recycle() } private fun getDrawableFromResource(array: TypedArray, styleableIndexId: Int): Drawable? { From c5ba74690471fa8763513592ae1744678ef25763 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 8 Jul 2020 11:00:13 +0200 Subject: [PATCH 02/41] Fixes #1647 share not working --- CHANGES.md | 2 +- .../home/room/detail/RoomDetailFragment.kt | 14 +++++++------- .../home/room/detail/RoomDetailViewModel.kt | 14 +++++++------- .../features/media/ImageMediaViewerActivity.kt | 14 +++++++------- .../riotx/features/media/VideoContentRenderer.kt | 2 +- .../features/media/VideoMediaViewerActivity.kt | 14 +++++++------- .../roomprofile/uploads/RoomUploadsViewModel.kt | 14 +++++++------- 7 files changed, 37 insertions(+), 37 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 04ac45c3cc..1200be4a12 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,7 +10,7 @@ Improvements 🙌: - Handling (almost) properly the groups fetching (#1634) Bugfix 🐛: - - + - Regression | Share action menu do not work (#1647) Translations 🗣: - diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 8c075004a9..d38a26c099 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -1337,13 +1337,13 @@ class RoomDetailFragment @Inject constructor( private fun onShareActionClicked(action: EventSharedAction.Share) { session.fileService().downloadFile( - FileService.DownloadMode.FOR_EXTERNAL_SHARE, - action.eventId, - action.messageContent.body, - action.messageContent.getFileUrl(), - action.messageContent.mimeType, - action.messageContent.encryptedFileInfo?.toElementToDecrypt(), - object : MatrixCallback { + downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, + id = action.eventId, + fileName = action.messageContent.body, + mimeType = action.messageContent.mimeType, + url = action.messageContent.getFileUrl(), + elementToDecrypt = action.messageContent.encryptedFileInfo?.toElementToDecrypt(), + callback = object : MatrixCallback { override fun onSuccess(data: File) { if (isAdded) { shareMedia(requireContext(), data, getMimeTypeFromUri(requireContext(), data.toUri())) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 62830a1c63..e2e7700d1f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -873,13 +873,13 @@ class RoomDetailViewModel @AssistedInject constructor( } } else { session.fileService().downloadFile( - FileService.DownloadMode.FOR_INTERNAL_USE, - action.eventId, - action.messageFileContent.getFileName(), - action.messageFileContent.mimeType, - mxcUrl, - action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(), - object : MatrixCallback { + downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, + id = action.eventId, + fileName = action.messageFileContent.getFileName(), + mimeType = action.messageFileContent.mimeType, + url = mxcUrl, + elementToDecrypt = action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(), + callback = object : MatrixCallback { override fun onSuccess(data: File) { _viewEvents.post(RoomDetailViewEvents.DownloadFileState( action.messageFileContent.mimeType, diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt index 2be940d0c1..092199759f 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt @@ -134,13 +134,13 @@ class ImageMediaViewerActivity : VectorBaseActivity() { private fun onShareActionClicked() { session.fileService().downloadFile( - FileService.DownloadMode.FOR_EXTERNAL_SHARE, - mediaData.eventId, - mediaData.filename, - mediaData.mimeType, - mediaData.url, - mediaData.elementToDecrypt, - object : MatrixCallback { + downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, + id = mediaData.eventId, + fileName = mediaData.filename, + mimeType = mediaData.mimeType, + url = mediaData.url, + elementToDecrypt = mediaData.elementToDecrypt, + callback = object : MatrixCallback { override fun onSuccess(data: File) { shareMedia(this@ImageMediaViewerActivity, data, getMimeTypeFromUri(this@ImageMediaViewerActivity, data.toUri())) } diff --git a/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt index eb9105f792..760d3b12a0 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt @@ -70,7 +70,7 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder: downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, id = data.eventId, fileName = data.filename, - mimeType = null, + mimeType = data.mimeType, url = data.url, elementToDecrypt = data.elementToDecrypt, callback = object : MatrixCallback { diff --git a/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt index 6ef8927f00..d9df861a25 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt @@ -79,13 +79,13 @@ class VideoMediaViewerActivity : VectorBaseActivity() { private fun onShareActionClicked() { session.fileService().downloadFile( - FileService.DownloadMode.FOR_EXTERNAL_SHARE, - mediaData.eventId, - mediaData.filename, - mediaData.mimeType, - mediaData.url, - mediaData.elementToDecrypt, - object : MatrixCallback { + downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, + id = mediaData.eventId, + fileName = mediaData.filename, + mimeType = mediaData.mimeType, + url = mediaData.url, + elementToDecrypt = mediaData.elementToDecrypt, + callback = object : MatrixCallback { override fun onSuccess(data: File) { shareMedia(this@VideoMediaViewerActivity, data, getMimeTypeFromUri(this@VideoMediaViewerActivity, data.toUri())) } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt index 10f0a5051e..7cc8b9b31d 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt @@ -158,13 +158,13 @@ class RoomUploadsViewModel @AssistedInject constructor( try { val file = awaitCallback { session.fileService().downloadFile( - FileService.DownloadMode.FOR_EXTERNAL_SHARE, - action.uploadEvent.eventId, - action.uploadEvent.contentWithAttachmentContent.body, - action.uploadEvent.contentWithAttachmentContent.getFileUrl(), - action.uploadEvent.contentWithAttachmentContent.mimeType, - action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(), - it) + downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, + id = action.uploadEvent.eventId, + fileName = action.uploadEvent.contentWithAttachmentContent.body, + mimeType = action.uploadEvent.contentWithAttachmentContent.mimeType, + url = action.uploadEvent.contentWithAttachmentContent.getFileUrl(), + elementToDecrypt = action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(), + callback = it) } _viewEvents.post(RoomUploadsViewEvents.FileReadyForSaving(file, action.uploadEvent.contentWithAttachmentContent.body)) } catch (failure: Throwable) { From 4a2a6d34aebbb2383a71eaa0ad99c24dac81cabd Mon Sep 17 00:00:00 2001 From: Valere Date: Sun, 5 Jul 2020 21:47:38 +0200 Subject: [PATCH 03/41] Initial commit --- attachment-viewer/.gitignore | 1 + attachment-viewer/build.gradle | 81 +++++++ attachment-viewer/consumer-rules.pro | 0 attachment-viewer/proguard-rules.pro | 21 ++ .../src/main/AndroidManifest.xml | 11 + .../AttachmentSourceProvider.kt | 36 +++ .../AttachmentViewerActivity.kt | 210 ++++++++++++++++++ .../attachment_viewer/AttachmentsAdapter.kt | 133 +++++++++++ .../attachment_viewer/ImageViewHolder.kt | 75 +++++++ .../riotx/attachment_viewer/SwipeDirection.kt | 38 ++++ .../SwipeDirectionDetector.kt | 90 ++++++++ .../SwipeToDismissHandler.kt | 126 +++++++++++ .../res/layout/activity_attachment_viewer.xml | 46 ++++ .../main/res/layout/item_image_attachment.xml | 22 ++ .../main/res/layout/item_video_attachment.xml | 26 +++ .../main/res/layout/view_image_attachment.xml | 17 ++ .../src/main/res/values/dimens.xml | 3 + .../src/main/res/values/strings.xml | 11 + .../src/main/res/values/styles.xml | 12 + build.gradle | 2 + .../session/room/timeline/TimelineService.kt | 2 + .../room/timeline/DefaultTimelineService.kt | 25 ++- settings.gradle | 4 +- vector/build.gradle | 5 + vector/src/main/AndroidManifest.xml | 5 + .../vector/riotx/core/di/ScreenComponent.kt | 2 + .../home/room/detail/RoomDetailFragment.kt | 2 +- .../features/media/ImageContentRenderer.kt | 63 ++++++ .../media/ImageMediaViewerActivity.kt | 2 + .../features/media/RoomAttachmentProvider.kt | 82 +++++++ .../media/VectorAttachmentViewerActivity.kt | 207 +++++++++++++++++ .../features/navigation/DefaultNavigator.kt | 56 +++-- .../riotx/features/navigation/Navigator.kt | 2 +- .../riotx/features/popup/PopupAlertManager.kt | 3 +- .../uploads/media/RoomUploadsMediaFragment.kt | 2 +- .../features/themes/ActivityOtherThemes.kt | 6 + vector/src/main/res/values/theme_common.xml | 11 + 37 files changed, 1409 insertions(+), 31 deletions(-) create mode 100644 attachment-viewer/.gitignore create mode 100644 attachment-viewer/build.gradle create mode 100644 attachment-viewer/consumer-rules.pro create mode 100644 attachment-viewer/proguard-rules.pro create mode 100644 attachment-viewer/src/main/AndroidManifest.xml create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentViewerActivity.kt create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentsAdapter.kt create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ImageViewHolder.kt create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirection.kt create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirectionDetector.kt create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeToDismissHandler.kt create mode 100644 attachment-viewer/src/main/res/layout/activity_attachment_viewer.xml create mode 100644 attachment-viewer/src/main/res/layout/item_image_attachment.xml create mode 100644 attachment-viewer/src/main/res/layout/item_video_attachment.xml create mode 100644 attachment-viewer/src/main/res/layout/view_image_attachment.xml create mode 100644 attachment-viewer/src/main/res/values/dimens.xml create mode 100644 attachment-viewer/src/main/res/values/strings.xml create mode 100644 attachment-viewer/src/main/res/values/styles.xml create mode 100644 vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt diff --git a/attachment-viewer/.gitignore b/attachment-viewer/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/attachment-viewer/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle new file mode 100644 index 0000000000..7fcda7a742 --- /dev/null +++ b/attachment-viewer/build.gradle @@ -0,0 +1,81 @@ +/* + * 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. + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +buildscript { + repositories { + maven { + url 'https://jitpack.io' + content { + // PhotoView + includeGroupByRegex 'com\\.github\\.chrisbanes' + } + } + jcenter() + } + +} + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.3" + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { +// implementation 'com.github.MikeOrtiz:TouchImageView:3.0.2' + implementation 'com.github.chrisbanes:PhotoView:2.0.0' + implementation "com.github.bumptech.glide:glide:4.10.0" + + implementation fileTree(dir: "libs", include: ["*.jar"]) + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.core:core-ktx:1.3.0' + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'com.google.android.material:material:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.navigation:navigation-fragment-ktx:2.1.0' + implementation 'androidx.navigation:navigation-ui-ktx:2.1.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + +} \ No newline at end of file diff --git a/attachment-viewer/consumer-rules.pro b/attachment-viewer/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/attachment-viewer/proguard-rules.pro b/attachment-viewer/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/attachment-viewer/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/attachment-viewer/src/main/AndroidManifest.xml b/attachment-viewer/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..4a632774f7 --- /dev/null +++ b/attachment-viewer/src/main/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt new file mode 100644 index 0000000000..9fd2902970 --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt @@ -0,0 +1,36 @@ +/* + * 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.riotx.attachment_viewer + +sealed class AttachmentInfo { + data class Image(val url: String, val data: Any?) : AttachmentInfo() + data class Video(val url: String, val data: Any) : AttachmentInfo() + data class Audio(val url: String, val data: Any) : AttachmentInfo() + data class File(val url: String, val data: Any) : AttachmentInfo() + + fun bind() { + } +} + +interface AttachmentSourceProvider { + + fun getItemCount(): Int + + fun getAttachmentInfoAt(position: Int): AttachmentInfo + + fun loadImage(holder: ImageViewHolder, info: AttachmentInfo.Image) +} diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentViewerActivity.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentViewerActivity.kt new file mode 100644 index 0000000000..2d4cbff00d --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentViewerActivity.kt @@ -0,0 +1,210 @@ +/* + * 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.riotx.attachment_viewer + +import android.graphics.Color +import android.os.Bundle +import android.util.Log +import android.view.MotionEvent +import android.view.ScaleGestureDetector +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import androidx.viewpager2.widget.ViewPager2 +import kotlinx.android.synthetic.main.activity_attachment_viewer.* +import kotlin.math.abs + +abstract class AttachmentViewerActivity : AppCompatActivity() { + + lateinit var pager2: ViewPager2 + lateinit var imageTransitionView: ImageView + lateinit var transitionImageContainer: ViewGroup + + // TODO + private var overlayView: View? = null + + private lateinit var swipeDismissHandler: SwipeToDismissHandler + private lateinit var directionDetector: SwipeDirectionDetector + private lateinit var scaleDetector: ScaleGestureDetector + + + var currentPosition = 0 + + private var swipeDirection: SwipeDirection? = null + + private fun isScaled() = attachmentsAdapter.isScaled(currentPosition) + + private var wasScaled: Boolean = false + private var isSwipeToDismissAllowed: Boolean = true + private lateinit var attachmentsAdapter: AttachmentsAdapter + +// private val shouldDismissToBottom: Boolean +// get() = e == null +// || !externalTransitionImageView.isRectVisible +// || !isAtStartPosition + + private var isImagePagerIdle = true + + fun setSourceProvider(sourceProvider: AttachmentSourceProvider) { + attachmentsAdapter.attachmentSourceProvider = sourceProvider + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_attachment_viewer) + attachmentPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL + attachmentsAdapter = AttachmentsAdapter() + attachmentPager.adapter = attachmentsAdapter + imageTransitionView = transitionImageView + transitionImageContainer = findViewById(R.id.transitionImageContainer) + pager2 = attachmentPager + directionDetector = createSwipeDirectionDetector() + + attachmentPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageScrollStateChanged(state: Int) { + isImagePagerIdle = state == ViewPager2.SCROLL_STATE_IDLE + } + + override fun onPageSelected(position: Int) { + currentPosition = position + } + }) + + swipeDismissHandler = createSwipeToDismissHandler() + rootContainer.setOnTouchListener(swipeDismissHandler) + rootContainer.viewTreeObserver.addOnGlobalLayoutListener { swipeDismissHandler.translationLimit = dismissContainer.height / 4 } + + scaleDetector = createScaleGestureDetector() + + } + + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + + // The zoomable view is configured to disallow interception when image is zoomed + + // Check if the overlay is visible, and wants to handle the click +// if (overlayView.isVisible && overlayView?.dispatchTouchEvent(event) == true) { +// return true +// } + + + Log.v("ATTACHEMENTS", "================\ndispatchTouchEvent $ev") + handleUpDownEvent(ev) + + Log.v("ATTACHEMENTS", "scaleDetector is in progress ${scaleDetector.isInProgress}") + Log.v("ATTACHEMENTS", "pointerCount ${ev.pointerCount}") + Log.v("ATTACHEMENTS", "wasScaled ${wasScaled}") + if (swipeDirection == null && (scaleDetector.isInProgress || ev.pointerCount > 1 || wasScaled)) { + wasScaled = true + Log.v("ATTACHEMENTS", "dispatch to pager") + return attachmentPager.dispatchTouchEvent(ev) + } + + + Log.v("ATTACHEMENTS", "is current item scaled ${isScaled()}") + return (if (isScaled()) super.dispatchTouchEvent(ev) else handleTouchIfNotScaled(ev)).also { + Log.v("ATTACHEMENTS", "\n================") + } + } + + private fun handleUpDownEvent(event: MotionEvent) { + Log.v("ATTACHEMENTS", "handleUpDownEvent $event") + if (event.action == MotionEvent.ACTION_UP) { + handleEventActionUp(event) + } + + if (event.action == MotionEvent.ACTION_DOWN) { + handleEventActionDown(event) + } + + scaleDetector.onTouchEvent(event) +// gestureDetector.onTouchEvent(event) + } + + private fun handleEventActionDown(event: MotionEvent) { + swipeDirection = null + wasScaled = false + attachmentPager.dispatchTouchEvent(event) + + swipeDismissHandler.onTouch(rootContainer, event) +// isOverlayWasClicked = dispatchOverlayTouch(event) + } + + private fun handleEventActionUp(event: MotionEvent) { +// wasDoubleTapped = false + swipeDismissHandler.onTouch(rootContainer, event) + attachmentPager.dispatchTouchEvent(event) +// isOverlayWasClicked = dispatchOverlayTouch(event) + } + + private fun handleTouchIfNotScaled(event: MotionEvent): Boolean { + + Log.v("ATTACHEMENTS", "handleTouchIfNotScaled ${event}") + directionDetector.handleTouchEvent(event) + + return when (swipeDirection) { + SwipeDirection.Up, SwipeDirection.Down -> { + if (isSwipeToDismissAllowed && !wasScaled && isImagePagerIdle) { + swipeDismissHandler.onTouch(rootContainer, event) + } else true + } + SwipeDirection.Left, SwipeDirection.Right -> { + attachmentPager.dispatchTouchEvent(event) + } + else -> true + } + } + + + private fun handleSwipeViewMove(translationY: Float, translationLimit: Int) { + val alpha = calculateTranslationAlpha(translationY, translationLimit) + backgroundView.alpha = alpha + dismissContainer.alpha = alpha + overlayView?.alpha = alpha + } + + private fun dispatchOverlayTouch(event: MotionEvent): Boolean = + overlayView + ?.let { it.isVisible && it.dispatchTouchEvent(event) } + ?: false + + private fun calculateTranslationAlpha(translationY: Float, translationLimit: Int): Float = + 1.0f - 1.0f / translationLimit.toFloat() / 4f * abs(translationY) + + private fun createSwipeToDismissHandler() + : SwipeToDismissHandler = SwipeToDismissHandler( + swipeView = dismissContainer, + shouldAnimateDismiss = { shouldAnimateDismiss() }, + onDismiss = { animateClose() }, + onSwipeViewMove = ::handleSwipeViewMove) + + private fun createSwipeDirectionDetector() = + SwipeDirectionDetector(this) { swipeDirection = it } + + private fun createScaleGestureDetector() = + ScaleGestureDetector(this, ScaleGestureDetector.SimpleOnScaleGestureListener()) + + + protected open fun shouldAnimateDismiss(): Boolean = true + + protected open fun animateClose() { + window.statusBarColor = Color.TRANSPARENT + finish() + } +} diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentsAdapter.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentsAdapter.kt new file mode 100644 index 0000000000..b9914e4dda --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentsAdapter.kt @@ -0,0 +1,133 @@ +/* + * 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.riotx.attachment_viewer + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView + + +abstract class BaseViewHolder constructor(itemView: View) : + RecyclerView.ViewHolder(itemView) { + + abstract fun bind(attachmentInfo: AttachmentInfo) +} + + +class AttachmentViewHolder constructor(itemView: View) : + BaseViewHolder(itemView) { + + override fun bind(attachmentInfo: AttachmentInfo) { + + } +} + +//class AttachmentsAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : FragmentStateAdapter(fragmentManager, lifecycle) { +class AttachmentsAdapter() : RecyclerView.Adapter() { + + var attachmentSourceProvider: AttachmentSourceProvider? = null + set(value) { + field = value + notifyDataSetChanged() + } + + var recyclerView: RecyclerView? = null + + override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { + this.recyclerView = recyclerView + } + + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + this.recyclerView = null + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { + val inflater = LayoutInflater.from(parent.context) + val itemView = inflater.inflate(viewType, parent, false) + return when (viewType) { + R.layout.item_image_attachment -> ImageViewHolder(itemView) + else -> AttachmentViewHolder(itemView) + } + } + + override fun getItemViewType(position: Int): Int { + val info = attachmentSourceProvider!!.getAttachmentInfoAt(position) + return when (info) { + is AttachmentInfo.Image -> R.layout.item_image_attachment + is AttachmentInfo.Video -> R.layout.item_video_attachment + is AttachmentInfo.Audio -> TODO() + is AttachmentInfo.File -> TODO() + } + + } + + override fun getItemCount(): Int { + return attachmentSourceProvider?.getItemCount() ?: 0 + } + + override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { + attachmentSourceProvider?.getAttachmentInfoAt(position)?.let { + holder.bind(it) + if (it is AttachmentInfo.Image) { + attachmentSourceProvider?.loadImage(holder as ImageViewHolder, it) + } + } + } + + fun isScaled(position: Int): Boolean { + val holder = recyclerView?.findViewHolderForAdapterPosition(position) + if (holder is ImageViewHolder) { + return holder.touchImageView.attacher.scale > 1f + } + return false + } + +// override fun getItemCount(): Int { +// return 8 +// } +// +// override fun createFragment(position: Int): Fragment { +// // Return a NEW fragment instance in createFragment(int) +// val fragment = DemoObjectFragment() +// fragment.arguments = Bundle().apply { +// // Our object is just an integer :-P +// putInt(ARG_OBJECT, position + 1) +// } +// return fragment +// } + +} + + +//private const val ARG_OBJECT = "object" +// +//// Instances of this class are fragments representing a single +//// object in our collection. +//class DemoObjectFragment : Fragment() { +// +// override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { +// return inflater.inflate(R.layout.view_image_attachment, container, false) +// } +// +// override fun onViewCreated(view: View, savedInstanceState: Bundle?) { +// arguments?.takeIf { it.containsKey(ARG_OBJECT) }?.apply { +// val textView: TextView = view.findViewById(R.id.testPage) +// textView.text = getInt(ARG_OBJECT).toString() +// } +// } +//} diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ImageViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ImageViewHolder.kt new file mode 100644 index 0000000000..cac6a4fd9e --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ImageViewHolder.kt @@ -0,0 +1,75 @@ +/* + * 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.riotx.attachment_viewer + +import android.graphics.drawable.Drawable +import android.util.Log +import android.view.View +import android.widget.LinearLayout +import android.widget.ProgressBar +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import com.bumptech.glide.request.target.CustomViewTarget +import com.bumptech.glide.request.transition.Transition +import com.github.chrisbanes.photoview.PhotoView + +class ImageViewHolder constructor(itemView: View) : + BaseViewHolder(itemView) { + + val touchImageView: PhotoView = itemView.findViewById(R.id.touchImageView) + val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress) + + init { + touchImageView.setAllowParentInterceptOnEdge(false) + touchImageView.setOnScaleChangeListener { scaleFactor, _, _ -> + Log.v("ATTACHEMENTS", "scaleFactor $scaleFactor") + // It's a bit annoying but when you pitch down the scaling + // is not exactly one :/ + touchImageView.setAllowParentInterceptOnEdge(scaleFactor <= 1.0008f) + } + touchImageView.setScale(1.0f, true) + touchImageView.setAllowParentInterceptOnEdge(true) + } + + val customTargetView = object : CustomViewTarget(touchImageView) { + + override fun onResourceLoading(placeholder: Drawable?) { + imageLoaderProgress.isVisible = true + } + + override fun onLoadFailed(errorDrawable: Drawable?) { + imageLoaderProgress.isVisible = false + } + + override fun onResourceCleared(placeholder: Drawable?) { + touchImageView.setImageDrawable(placeholder) + } + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + imageLoaderProgress.isVisible = false + // Glide mess up the view size :/ + touchImageView.updateLayoutParams { + width = LinearLayout.LayoutParams.MATCH_PARENT + height = LinearLayout.LayoutParams.MATCH_PARENT + } + touchImageView.setImageDrawable(resource) + } + } + + override fun bind(attachmentInfo: AttachmentInfo) { + } +} diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirection.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirection.kt new file mode 100644 index 0000000000..fc54d292c2 --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirection.kt @@ -0,0 +1,38 @@ +/* + * 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.riotx.attachment_viewer + +sealed class SwipeDirection { + object NotDetected : SwipeDirection() + object Up : SwipeDirection() + object Down : SwipeDirection() + object Left : SwipeDirection() + object Right : SwipeDirection() + + companion object { + fun fromAngle(angle: Double): SwipeDirection { + return when (angle) { + in 0.0..45.0 -> Right + in 45.0..135.0 -> Up + in 135.0..225.0 -> Left + in 225.0..315.0 -> Down + in 315.0..360.0 -> Right + else -> NotDetected + } + } + } +} diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirectionDetector.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirectionDetector.kt new file mode 100644 index 0000000000..cce37a6d05 --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirectionDetector.kt @@ -0,0 +1,90 @@ +/* + * 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.riotx.attachment_viewer + +import android.content.Context +import android.view.MotionEvent +import kotlin.math.sqrt + +class SwipeDirectionDetector( + context: Context, + private val onDirectionDetected: (SwipeDirection) -> Unit +) { + + private val touchSlop: Int = android.view.ViewConfiguration.get(context).scaledTouchSlop + private var startX: Float = 0f + private var startY: Float = 0f + private var isDetected: Boolean = false + + fun handleTouchEvent(event: MotionEvent) { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + startX = event.x + startY = event.y + } + MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { + if (!isDetected) { + onDirectionDetected(SwipeDirection.NotDetected) + } + startY = 0.0f + startX = startY + isDetected = false + } + MotionEvent.ACTION_MOVE -> if (!isDetected && getEventDistance(event) > touchSlop) { + isDetected = true + onDirectionDetected(getDirection(startX, startY, event.x, event.y)) + } + } + } + + /** + * Given two points in the plane p1=(x1, x2) and p2=(y1, y1), this method + * returns the direction that an arrow pointing from p1 to p2 would have. + * + * @param x1 the x position of the first point + * @param y1 the y position of the first point + * @param x2 the x position of the second point + * @param y2 the y position of the second point + * @return the direction + */ + private fun getDirection(x1: Float, y1: Float, x2: Float, y2: Float): SwipeDirection { + val angle = getAngle(x1, y1, x2, y2) + return SwipeDirection.fromAngle(angle) + } + + /** + * Finds the angle between two points in the plane (x1,y1) and (x2, y2) + * The angle is measured with 0/360 being the X-axis to the right, angles + * increase counter clockwise. + * + * @param x1 the x position of the first point + * @param y1 the y position of the first point + * @param x2 the x position of the second point + * @param y2 the y position of the second point + * @return the angle between two points + */ + private fun getAngle(x1: Float, y1: Float, x2: Float, y2: Float): Double { + val rad = Math.atan2((y1 - y2).toDouble(), (x2 - x1).toDouble()) + Math.PI + return (rad * 180 / Math.PI + 180) % 360 + } + + private fun getEventDistance(ev: MotionEvent): Float { + val dx = ev.getX(0) - startX + val dy = ev.getY(0) - startY + return sqrt((dx * dx + dy * dy).toDouble()).toFloat() + } +} diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeToDismissHandler.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeToDismissHandler.kt new file mode 100644 index 0000000000..3a317d94e2 --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeToDismissHandler.kt @@ -0,0 +1,126 @@ +/* + * 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.riotx.attachment_viewer + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.annotation.SuppressLint +import android.graphics.Rect +import android.view.MotionEvent +import android.view.View +import android.view.ViewPropertyAnimator +import android.view.animation.AccelerateInterpolator + +class SwipeToDismissHandler( + private val swipeView: View, + private val onDismiss: () -> Unit, + private val onSwipeViewMove: (translationY: Float, translationLimit: Int) -> Unit, + private val shouldAnimateDismiss: () -> Boolean +) : View.OnTouchListener { + + companion object { + private const val ANIMATION_DURATION = 200L + } + + var translationLimit: Int = swipeView.height / 4 + private var isTracking = false + private var startY: Float = 0f + + @SuppressLint("ClickableViewAccessibility") + override fun onTouch(v: View, event: MotionEvent): Boolean { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + if (swipeView.hitRect.contains(event.x.toInt(), event.y.toInt())) { + isTracking = true + } + startY = event.y + return true + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + if (isTracking) { + isTracking = false + onTrackingEnd(v.height) + } + return true + } + MotionEvent.ACTION_MOVE -> { + if (isTracking) { + val translationY = event.y - startY + swipeView.translationY = translationY + onSwipeViewMove(translationY, translationLimit) + } + return true + } + else -> { + return false + } + } + } + + internal fun initiateDismissToBottom() { + animateTranslation(swipeView.height.toFloat()) + } + + private fun onTrackingEnd(parentHeight: Int) { + val animateTo = when { + swipeView.translationY < -translationLimit -> -parentHeight.toFloat() + swipeView.translationY > translationLimit -> parentHeight.toFloat() + else -> 0f + } + + if (animateTo != 0f && !shouldAnimateDismiss()) { + onDismiss() + } else { + animateTranslation(animateTo) + } + } + + private fun animateTranslation(translationTo: Float) { + swipeView.animate() + .translationY(translationTo) + .setDuration(ANIMATION_DURATION) + .setInterpolator(AccelerateInterpolator()) + .setUpdateListener { onSwipeViewMove(swipeView.translationY, translationLimit) } + .setAnimatorListener(onAnimationEnd = { + if (translationTo != 0f) { + onDismiss() + } + + //remove the update listener, otherwise it will be saved on the next animation execution: + swipeView.animate().setUpdateListener(null) + }) + .start() + } +} + +internal fun ViewPropertyAnimator.setAnimatorListener( + onAnimationEnd: ((Animator?) -> Unit)? = null, + onAnimationStart: ((Animator?) -> Unit)? = null +) = this.setListener( + object : AnimatorListenerAdapter() { + + override fun onAnimationEnd(animation: Animator?) { + onAnimationEnd?.invoke(animation) + } + + override fun onAnimationStart(animation: Animator?) { + onAnimationStart?.invoke(animation) + } + }) + +internal val View?.hitRect: Rect + get() = Rect().also { this?.getHitRect(it) } diff --git a/attachment-viewer/src/main/res/layout/activity_attachment_viewer.xml b/attachment-viewer/src/main/res/layout/activity_attachment_viewer.xml new file mode 100644 index 0000000000..a8a68db1a5 --- /dev/null +++ b/attachment-viewer/src/main/res/layout/activity_attachment_viewer.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/attachment-viewer/src/main/res/layout/item_image_attachment.xml b/attachment-viewer/src/main/res/layout/item_image_attachment.xml new file mode 100644 index 0000000000..91a009df2a --- /dev/null +++ b/attachment-viewer/src/main/res/layout/item_image_attachment.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/attachment-viewer/src/main/res/layout/item_video_attachment.xml b/attachment-viewer/src/main/res/layout/item_video_attachment.xml new file mode 100644 index 0000000000..9449ec2e9f --- /dev/null +++ b/attachment-viewer/src/main/res/layout/item_video_attachment.xml @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/attachment-viewer/src/main/res/layout/view_image_attachment.xml b/attachment-viewer/src/main/res/layout/view_image_attachment.xml new file mode 100644 index 0000000000..3518a4472d --- /dev/null +++ b/attachment-viewer/src/main/res/layout/view_image_attachment.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/attachment-viewer/src/main/res/values/dimens.xml b/attachment-viewer/src/main/res/values/dimens.xml new file mode 100644 index 0000000000..125df87119 --- /dev/null +++ b/attachment-viewer/src/main/res/values/dimens.xml @@ -0,0 +1,3 @@ + + 16dp + \ No newline at end of file diff --git a/attachment-viewer/src/main/res/values/strings.xml b/attachment-viewer/src/main/res/values/strings.xml new file mode 100644 index 0000000000..6dcb56555a --- /dev/null +++ b/attachment-viewer/src/main/res/values/strings.xml @@ -0,0 +1,11 @@ + + AttachementViewerActivity + + First Fragment + Second Fragment + Next + Previous + + Hello first fragment + Hello second fragment. Arg: %1$s + \ No newline at end of file diff --git a/attachment-viewer/src/main/res/values/styles.xml b/attachment-viewer/src/main/res/values/styles.xml new file mode 100644 index 0000000000..a81174782e --- /dev/null +++ b/attachment-viewer/src/main/res/values/styles.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index af3952b2d3..47b3ab240d 100644 --- a/build.gradle +++ b/build.gradle @@ -39,6 +39,8 @@ allprojects { includeGroupByRegex "com\\.github\\.yalantis" // JsonViewer includeGroupByRegex 'com\\.github\\.BillCarsonFr' + // PhotoView + includeGroupByRegex 'com\\.github\\.chrisbanes' } } maven { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt index a69127532e..bdbbbf11bd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt @@ -39,4 +39,6 @@ interface TimelineService { fun getTimeLineEvent(eventId: String): TimelineEvent? fun getTimeLineEventLive(eventId: String): LiveData> + + fun getAttachementMessages() : List } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index 5723568197..ebdb8dd24d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -21,19 +21,24 @@ import androidx.lifecycle.Transformations import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.events.model.isImageMessage import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineService import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.toOptional +import im.vector.matrix.android.internal.crypto.store.db.doWithRealm import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.fetchCopyMap +import io.realm.Sort +import io.realm.kotlin.where import org.greenrobot.eventbus.EventBus internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String, @@ -73,10 +78,10 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv override fun getTimeLineEvent(eventId: String): TimelineEvent? { return monarchy .fetchCopyMap({ - TimelineEventEntity.where(it, roomId = roomId, eventId = eventId).findFirst() - }, { entity, _ -> - timelineEventMapper.map(entity) - }) + TimelineEventEntity.where(it, roomId = roomId, eventId = eventId).findFirst() + }, { entity, _ -> + timelineEventMapper.map(entity) + }) } override fun getTimeLineEventLive(eventId: String): LiveData> { @@ -88,4 +93,16 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv events.firstOrNull().toOptional() } } + + override fun getAttachementMessages(): List { + // TODO pretty bad query.. maybe we should denormalize clear type in base? + return doWithRealm(monarchy.realmConfiguration) { realm -> + realm.where() + .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) + .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) + .findAll() + ?.mapNotNull { timelineEventMapper.map(it).takeIf { it.root.isImageMessage() } } + ?: emptyList() + } + } } diff --git a/settings.gradle b/settings.gradle index 04307e89d9..3a7aa9ac1c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ -include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch' -include ':multipicker' +include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch', ':attachment-viewer' +include ':multipicker' \ No newline at end of file diff --git a/vector/build.gradle b/vector/build.gradle index 59ae3d35de..b409a7d8b8 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -279,6 +279,7 @@ dependencies { implementation project(":matrix-sdk-android-rx") implementation project(":diff-match-patch") implementation project(":multipicker") + implementation project(":attachment-viewer") implementation 'com.android.support:multidex:1.0.3' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" @@ -368,6 +369,10 @@ dependencies { implementation "com.github.piasy:GlideImageLoader:$big_image_viewer_version" implementation "com.github.piasy:ProgressPieIndicator:$big_image_viewer_version" implementation "com.github.piasy:GlideImageViewFactory:$big_image_viewer_version" + + // implementation 'com.github.MikeOrtiz:TouchImageView:3.0.2' + implementation 'com.github.chrisbanes:PhotoView:2.0.0' + implementation "com.github.bumptech.glide:glide:$glide_version" kapt "com.github.bumptech.glide:compiler:$glide_version" implementation 'com.danikula:videocache:2.7.1' diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index f9b78db17c..155c3bcd64 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -85,6 +85,11 @@ + + + + navigator.openImageViewer(requireActivity(), roomDetailArgs.roomId, mediaData, view) { pairs -> pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: "")) pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: "")) } diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt index eeeb55ed15..7cd7ba56e5 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt @@ -19,11 +19,13 @@ package im.vector.riotx.features.media import android.graphics.drawable.Drawable import android.net.Uri import android.os.Parcelable +import android.view.View import android.widget.ImageView import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.CustomViewTarget import com.bumptech.glide.request.target.Target import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.ORIENTATION_USE_EXIF import com.github.piasy.biv.view.BigImageView @@ -93,6 +95,25 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: .into(imageView) } + fun render(data: Data, contextView: View, target: CustomViewTarget<*, Drawable>) { + val req = if (data.elementToDecrypt != null) { + // Encrypted image + GlideApp + .with(contextView) + .load(data) + } else { + // Clear image + val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver().resolveFullSize(data.url) + GlideApp + .with(contextView) + .load(resolvedUrl) + } + + req.override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) + .fitCenter() + .into(target) + } + fun renderFitTarget(data: Data, mode: Mode, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) { val size = processSize(data, mode) @@ -122,6 +143,48 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: .into(imageView) } + fun renderThumbnailDontTransform(data: Data, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) { + + // a11y + imageView.contentDescription = data.filename + + val req = if (data.elementToDecrypt != null) { + // Encrypted image + GlideApp + .with(imageView) + .load(data) + } else { + // Clear image + val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver().resolveFullSize(data.url) + GlideApp + .with(imageView) + .load(resolvedUrl) + } + + req.listener(object : RequestListener { + override fun onLoadFailed(e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean): Boolean { + callback?.invoke(false) + return false + } + + override fun onResourceReady(resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean): Boolean { + callback?.invoke(true) + return false + } + }) + .dontTransform() + .into(imageView) + + + } + private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest { return if (data.elementToDecrypt != null) { // Encrypted image diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt index 092199759f..8a6c2f7545 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt @@ -91,6 +91,8 @@ class ImageMediaViewerActivity : VectorBaseActivity() { encryptedImageView.isVisible = false // Postpone transaction a bit until thumbnail is loaded supportPostponeEnterTransition() + + // We are not passing the exact same image that in the imageContentRenderer.renderFitTarget(mediaData, ImageContentRenderer.Mode.THUMBNAIL, imageTransitionView) { // Proceed with transaction scheduleStartPostponedTransition(imageTransitionView) diff --git a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt new file mode 100644 index 0000000000..991ecaafde --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt @@ -0,0 +1,82 @@ +/* + * 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.riotx.features.media + +import android.graphics.drawable.Drawable +import com.bumptech.glide.request.target.CustomViewTarget +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent +import im.vector.matrix.android.api.session.room.model.message.getFileUrl +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt +import im.vector.riotx.attachment_viewer.AttachmentInfo +import im.vector.riotx.attachment_viewer.AttachmentSourceProvider +import im.vector.riotx.attachment_viewer.ImageViewHolder +import javax.inject.Inject + +class RoomAttachmentProvider( + private val attachments: List, + private val initialIndex: Int, + private val imageContentRenderer: ImageContentRenderer +) : AttachmentSourceProvider { + + override fun getItemCount(): Int { + return attachments.size + } + + override fun getAttachmentInfoAt(position: Int): AttachmentInfo { + return attachments[position].let { + val content = it.root.getClearContent().toModel() as? MessageWithAttachmentContent + val data = ImageContentRenderer.Data( + eventId = it.eventId, + filename = content?.body ?: "", + mimeType = content?.mimeType, + url = content?.getFileUrl(), + elementToDecrypt = content?.encryptedFileInfo?.toElementToDecrypt(), + maxHeight = -1, + maxWidth = -1, + width = null, + height = null + ) + AttachmentInfo.Image( + content?.url ?: "", + data + ) + } + } + + override fun loadImage(holder: ImageViewHolder, info: AttachmentInfo.Image) { + (info.data as? ImageContentRenderer.Data)?.let { + imageContentRenderer.render(it, holder.touchImageView, holder.customTargetView as CustomViewTarget<*, Drawable>) + } + } +// override fun loadImage(holder: ImageViewHolder, info: AttachmentInfo.Image) { +// (info.data as? ImageContentRenderer.Data)?.let { +// imageContentRenderer.render(it, ImageContentRenderer.Mode.FULL_SIZE, holder.touchImageView) +// } +// } +} + +class RoomAttachmentProviderFactory @Inject constructor( + private val imageContentRenderer: ImageContentRenderer +) { + + fun createProvider(attachments: List, initialIndex: Int): RoomAttachmentProvider { + return RoomAttachmentProvider(attachments, initialIndex, imageContentRenderer) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt new file mode 100644 index 0000000000..2df8bfd0f6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt @@ -0,0 +1,207 @@ +/* + * 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.riotx.features.media + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +import android.view.View +import android.view.ViewTreeObserver +import androidx.core.app.ActivityCompat +import androidx.core.transition.addListener +import androidx.core.view.ViewCompat +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import androidx.transition.Transition +import im.vector.riotx.attachment_viewer.AttachmentViewerActivity +import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.di.DaggerScreenComponent +import im.vector.riotx.core.di.HasVectorInjector +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.di.VectorComponent +import im.vector.riotx.features.themes.ActivityOtherThemes +import im.vector.riotx.features.themes.ThemeUtils +import kotlinx.android.parcel.Parcelize +import timber.log.Timber +import javax.inject.Inject +import kotlin.system.measureTimeMillis + +class VectorAttachmentViewerActivity : AttachmentViewerActivity() { + + @Parcelize + data class Args( + val roomId: String?, + val eventId: String, + val sharedTransitionName: String? + ) : Parcelable + + @Inject + lateinit var sessionHolder: ActiveSessionHolder + + @Inject + lateinit var dataSourceFactory: RoomAttachmentProviderFactory + + @Inject + lateinit var imageContentRenderer: ImageContentRenderer + + private lateinit var screenComponent: ScreenComponent + + private var initialIndex = 0 + private var isAnimatingOut = false + + override fun onCreate(savedInstanceState: Bundle?) { + + super.onCreate(savedInstanceState) + Timber.i("onCreate Activity ${this.javaClass.simpleName}") + val vectorComponent = getVectorComponent() + screenComponent = DaggerScreenComponent.factory().create(vectorComponent, this) + val timeForInjection = measureTimeMillis { + screenComponent.inject(this) + } + Timber.v("Injecting dependencies into ${javaClass.simpleName} took $timeForInjection ms") + ThemeUtils.setActivityTheme(this, getOtherThemes()) + + val args = args() ?: throw IllegalArgumentException("Missing arguments") + val session = sessionHolder.getSafeActiveSession() ?: return Unit.also { finish() } + + val room = args.roomId?.let { session.getRoom(it) } + val events = room?.getAttachementMessages() ?: emptyList() + val index = events.indexOfFirst { it.eventId == args.eventId } + initialIndex = index + + + if (savedInstanceState == null && addTransitionListener()) { + args.sharedTransitionName?.let { + ViewCompat.setTransitionName(imageTransitionView, it) + transitionImageContainer.isVisible = true + + // Postpone transaction a bit until thumbnail is loaded + val mediaData: ImageContentRenderer.Data? = intent.getParcelableExtra(EXTRA_IMAGE_DATA) + if (mediaData != null) { + // will be shown at end of transition + pager2.isInvisible = true + supportPostponeEnterTransition() + imageContentRenderer.renderThumbnailDontTransform(mediaData, imageTransitionView) { + // Proceed with transaction + scheduleStartPostponedTransition(imageTransitionView) + } + } + } + } + + setSourceProvider(dataSourceFactory.createProvider(events, index)) + if (savedInstanceState == null) { + pager2.setCurrentItem(index, false) + } + + } + + private fun getOtherThemes() = ActivityOtherThemes.VectorAttachmentsPreview + + + override fun shouldAnimateDismiss(): Boolean { + return currentPosition != initialIndex + } + + override fun onBackPressed() { + if (currentPosition == initialIndex) { + // show back the transition view + // TODO, we should track and update the mapping + transitionImageContainer.isVisible = true + } + isAnimatingOut = true + super.onBackPressed() + } + + override fun animateClose() { + if (currentPosition == initialIndex) { + // show back the transition view + // TODO, we should track and update the mapping + transitionImageContainer.isVisible = true + } + isAnimatingOut = true + ActivityCompat.finishAfterTransition(this); + } + + /* ========================================================================================== + * PRIVATE METHODS + * ========================================================================================== */ + + /** + * Try and add a [Transition.TransitionListener] to the entering shared element + * [Transition]. We do this so that we can load the full-size image after the transition + * has completed. + * + * @return true if we were successful in adding a listener to the enter transition + */ + private fun addTransitionListener(): Boolean { + val transition = window.sharedElementEnterTransition + + if (transition != null) { + // There is an entering shared element transition so add a listener to it + transition.addListener( + onEnd = { + if (!isAnimatingOut) { + // The listener is also called when we are exiting + transitionImageContainer.isVisible = false + pager2.isInvisible = false + } + }, + onCancel = { + if (!isAnimatingOut) { + transitionImageContainer.isVisible = false + pager2.isInvisible = false + } + } + ) + return true + } + + // If we reach here then we have not added a listener + return false + } + + private fun args() = intent.getParcelableExtra(EXTRA_ARGS) + + + private fun getVectorComponent(): VectorComponent { + return (application as HasVectorInjector).injector() + } + + private fun scheduleStartPostponedTransition(sharedElement: View) { + sharedElement.viewTreeObserver.addOnPreDrawListener( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + sharedElement.viewTreeObserver.removeOnPreDrawListener(this) + supportStartPostponedEnterTransition() + return true + } + }) + } + + companion object { + + const val EXTRA_ARGS = "EXTRA_ARGS" + const val EXTRA_IMAGE_DATA = "EXTRA_IMAGE_DATA" + + fun newIntent(context: Context, mediaData: ImageContentRenderer.Data, roomId: String?, eventId: String, sharedTransitionName: String?) = Intent(context, VectorAttachmentViewerActivity::class.java).also { + it.putExtra(EXTRA_ARGS, Args(roomId, eventId, sharedTransitionName)) + it.putExtra(EXTRA_IMAGE_DATA, mediaData) + } + + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 0b89ab8ec4..debd58e6d2 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -49,11 +49,7 @@ import im.vector.riotx.features.home.room.detail.RoomDetailArgs import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.invite.InviteUsersToRoomActivity -import im.vector.riotx.features.media.BigImageViewerActivity -import im.vector.riotx.features.media.ImageContentRenderer -import im.vector.riotx.features.media.ImageMediaViewerActivity -import im.vector.riotx.features.media.VideoContentRenderer -import im.vector.riotx.features.media.VideoMediaViewerActivity +import im.vector.riotx.features.media.* import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity @@ -89,7 +85,8 @@ class DefaultNavigator @Inject constructor( override fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) { val session = sessionHolder.getSafeActiveSession() ?: return - val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId) ?: return + val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId) + ?: return (tx as? IncomingSasVerificationTransaction)?.performAccept() if (context is VectorBaseActivity) { VerificationBottomSheet.withArgs( @@ -216,7 +213,8 @@ class DefaultNavigator @Inject constructor( ?.let { avatarUrl -> val intent = BigImageViewerActivity.newIntent(activity, matrixItem.getBestName(), avatarUrl) val options = sharedElement?.let { - ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it) ?: "") + ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it) + ?: "") } activity.startActivity(intent, options?.toBundle()) } @@ -244,22 +242,38 @@ class DefaultNavigator @Inject constructor( context.startActivity(WidgetActivity.newIntent(context, widgetArgs)) } - override fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?) { - val intent = ImageMediaViewerActivity.newIntent(activity, mediaData, ViewCompat.getTransitionName(view)) - val pairs = ArrayList>() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let { - pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) + override fun openImageViewer(activity: Activity, roomId: String?, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?) { + VectorAttachmentViewerActivity.newIntent(activity, mediaData, roomId, mediaData.eventId, ViewCompat.getTransitionName(view)).let { intent -> + val pairs = ArrayList>() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let { + pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) + } + activity.window.decorView.findViewById(android.R.id.navigationBarBackground)?.let { + pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) + } } - activity.window.decorView.findViewById(android.R.id.navigationBarBackground)?.let { - pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) - } - } - pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: "")) - options?.invoke(pairs) + pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: "")) + options?.invoke(pairs) - val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle() - activity.startActivity(intent, bundle) + val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle() + activity.startActivity(intent, bundle) + } +// val intent = ImageMediaViewerActivity.newIntent(activity, mediaData, ViewCompat.getTransitionName(view)) +// val pairs = ArrayList>() +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { +// activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let { +// pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) +// } +// activity.window.decorView.findViewById(android.R.id.navigationBarBackground)?.let { +// pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) +// } +// } +// pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: "")) +// options?.invoke(pairs) +// +// val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle() +// activity.startActivity(intent, bundle) } override fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data) { diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index ce4d5ef3ea..54c0f55a7b 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -91,7 +91,7 @@ interface Navigator { fun openRoomWidget(context: Context, roomId: String, widget: Widget) - fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?) + fun openImageViewer(activity: Activity, roomId: String?, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?) fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data) } diff --git a/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt index 78a0cece41..e5b2f34f61 100644 --- a/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt +++ b/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt @@ -26,6 +26,7 @@ import com.tapadoo.alerter.Alerter import com.tapadoo.alerter.OnHideAlertListener import dagger.Lazy import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.themes.ThemeUtils import timber.log.Timber @@ -83,7 +84,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy + + \ No newline at end of file From 2d4a728af47898f77579b26530b5519ae5caac42 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 6 Jul 2020 09:45:42 +0200 Subject: [PATCH 04/41] Gif support --- .../AnimatedImageViewHolder.kt | 68 +++++++++++++++++++ .../AttachmentSourceProvider.kt | 4 +- .../attachment_viewer/AttachmentsAdapter.kt | 17 +++-- ...ewHolder.kt => ZoomableImageViewHolder.kt} | 2 +- .../layout/item_animated_image_attachment.xml | 22 ++++++ .../features/media/RoomAttachmentProvider.kt | 26 +++++-- 6 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AnimatedImageViewHolder.kt rename attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/{ImageViewHolder.kt => ZoomableImageViewHolder.kt} (97%) create mode 100644 attachment-viewer/src/main/res/layout/item_animated_image_attachment.xml diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AnimatedImageViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AnimatedImageViewHolder.kt new file mode 100644 index 0000000000..10b3cf8ffc --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AnimatedImageViewHolder.kt @@ -0,0 +1,68 @@ +/* + * 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.riotx.attachment_viewer + +import android.graphics.drawable.Animatable +import android.graphics.drawable.Drawable +import android.util.Log +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.ProgressBar +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import com.bumptech.glide.request.target.CustomViewTarget +import com.bumptech.glide.request.transition.Transition +import com.github.chrisbanes.photoview.PhotoView + +class AnimatedImageViewHolder constructor(itemView: View) : + BaseViewHolder(itemView) { + + val touchImageView: ImageView = itemView.findViewById(R.id.imageView) + val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress) + + val customTargetView = object : CustomViewTarget(touchImageView) { + + override fun onResourceLoading(placeholder: Drawable?) { + imageLoaderProgress.isVisible = true + } + + override fun onLoadFailed(errorDrawable: Drawable?) { + imageLoaderProgress.isVisible = false + } + + override fun onResourceCleared(placeholder: Drawable?) { + touchImageView.setImageDrawable(placeholder) + } + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + imageLoaderProgress.isVisible = false + // Glide mess up the view size :/ + touchImageView.updateLayoutParams { + width = LinearLayout.LayoutParams.MATCH_PARENT + height = LinearLayout.LayoutParams.MATCH_PARENT + } + touchImageView.setImageDrawable(resource) + if (resource is Animatable) { + resource.start(); + } + } + } + + override fun bind(attachmentInfo: AttachmentInfo) { + } +} diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt index 9fd2902970..f88083f818 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt @@ -18,6 +18,7 @@ package im.vector.riotx.attachment_viewer sealed class AttachmentInfo { data class Image(val url: String, val data: Any?) : AttachmentInfo() + data class AnimatedImage(val url: String, val data: Any?) : AttachmentInfo() data class Video(val url: String, val data: Any) : AttachmentInfo() data class Audio(val url: String, val data: Any) : AttachmentInfo() data class File(val url: String, val data: Any) : AttachmentInfo() @@ -32,5 +33,6 @@ interface AttachmentSourceProvider { fun getAttachmentInfoAt(position: Int): AttachmentInfo - fun loadImage(holder: ImageViewHolder, info: AttachmentInfo.Image) + fun loadImage(holder: ZoomableImageViewHolder, info: AttachmentInfo.Image) + fun loadImage(holder: AnimatedImageViewHolder, info: AttachmentInfo.AnimatedImage) } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentsAdapter.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentsAdapter.kt index b9914e4dda..f762a6ea3e 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentsAdapter.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentsAdapter.kt @@ -60,7 +60,8 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { val inflater = LayoutInflater.from(parent.context) val itemView = inflater.inflate(viewType, parent, false) return when (viewType) { - R.layout.item_image_attachment -> ImageViewHolder(itemView) + R.layout.item_image_attachment -> ZoomableImageViewHolder(itemView) + R.layout.item_animated_image_attachment -> AnimatedImageViewHolder(itemView) else -> AttachmentViewHolder(itemView) } } @@ -70,6 +71,7 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { return when (info) { is AttachmentInfo.Image -> R.layout.item_image_attachment is AttachmentInfo.Video -> R.layout.item_video_attachment + is AttachmentInfo.AnimatedImage -> R.layout.item_animated_image_attachment is AttachmentInfo.Audio -> TODO() is AttachmentInfo.File -> TODO() } @@ -83,15 +85,22 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { attachmentSourceProvider?.getAttachmentInfoAt(position)?.let { holder.bind(it) - if (it is AttachmentInfo.Image) { - attachmentSourceProvider?.loadImage(holder as ImageViewHolder, it) + when(it) { + is AttachmentInfo.Image -> { + attachmentSourceProvider?.loadImage(holder as ZoomableImageViewHolder, it) + } + is AttachmentInfo.AnimatedImage -> { + attachmentSourceProvider?.loadImage(holder as AnimatedImageViewHolder, it) + } + else -> {} } + } } fun isScaled(position: Int): Boolean { val holder = recyclerView?.findViewHolderForAdapterPosition(position) - if (holder is ImageViewHolder) { + if (holder is ZoomableImageViewHolder) { return holder.touchImageView.attacher.scale > 1f } return false diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ImageViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ZoomableImageViewHolder.kt similarity index 97% rename from attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ImageViewHolder.kt rename to attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ZoomableImageViewHolder.kt index cac6a4fd9e..6dd387b870 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ImageViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ZoomableImageViewHolder.kt @@ -27,7 +27,7 @@ import com.bumptech.glide.request.target.CustomViewTarget import com.bumptech.glide.request.transition.Transition import com.github.chrisbanes.photoview.PhotoView -class ImageViewHolder constructor(itemView: View) : +class ZoomableImageViewHolder constructor(itemView: View) : BaseViewHolder(itemView) { val touchImageView: PhotoView = itemView.findViewById(R.id.touchImageView) diff --git a/attachment-viewer/src/main/res/layout/item_animated_image_attachment.xml b/attachment-viewer/src/main/res/layout/item_animated_image_attachment.xml new file mode 100644 index 0000000000..1096267124 --- /dev/null +++ b/attachment-viewer/src/main/res/layout/item_animated_image_attachment.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt index 991ecaafde..079c435001 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt @@ -24,9 +24,10 @@ import im.vector.matrix.android.api.session.room.model.message.MessageWithAttach import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt +import im.vector.riotx.attachment_viewer.AnimatedImageViewHolder import im.vector.riotx.attachment_viewer.AttachmentInfo import im.vector.riotx.attachment_viewer.AttachmentSourceProvider -import im.vector.riotx.attachment_viewer.ImageViewHolder +import im.vector.riotx.attachment_viewer.ZoomableImageViewHolder import javax.inject.Inject class RoomAttachmentProvider( @@ -53,14 +54,27 @@ class RoomAttachmentProvider( width = null, height = null ) - AttachmentInfo.Image( - content?.url ?: "", - data - ) + if (content?.mimeType == "image/gif") { + AttachmentInfo.AnimatedImage( + content.url ?: "", + data + ) + } else { + AttachmentInfo.Image( + content?.url ?: "", + data + ) + } } } - override fun loadImage(holder: ImageViewHolder, info: AttachmentInfo.Image) { + override fun loadImage(holder: ZoomableImageViewHolder, info: AttachmentInfo.Image) { + (info.data as? ImageContentRenderer.Data)?.let { + imageContentRenderer.render(it, holder.touchImageView, holder.customTargetView as CustomViewTarget<*, Drawable>) + } + } + + override fun loadImage(holder: AnimatedImageViewHolder, info: AttachmentInfo.AnimatedImage) { (info.data as? ImageContentRenderer.Data)?.let { imageContentRenderer.render(it, holder.touchImageView, holder.customTargetView as CustomViewTarget<*, Drawable>) } From 76133ab55e08838d59eec730d9f0d9aac8d28f55 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 6 Jul 2020 15:47:15 +0200 Subject: [PATCH 05/41] Simple overlay --- attachment-viewer/build.gradle | 4 - .../AttachmentSourceProvider.kt | 6 ++ .../AttachmentViewerActivity.kt | 49 +++++++++-- .../features/media/AttachmentOverlayView.kt | 58 +++++++++++++ .../features/media/RoomAttachmentProvider.kt | 41 ++++++++- .../media/VectorAttachmentViewerActivity.kt | 24 ++++-- .../layout/merge_image_attachment_overlay.xml | 83 +++++++++++++++++++ vector/src/main/res/values/colors_riotx.xml | 1 + 8 files changed, 244 insertions(+), 22 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt create mode 100644 vector/src/main/res/layout/merge_image_attachment_overlay.xml diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle index 7fcda7a742..ac41c3ed75 100644 --- a/attachment-viewer/build.gradle +++ b/attachment-viewer/build.gradle @@ -34,16 +34,12 @@ buildscript { android { compileSdkVersion 29 - buildToolsVersion "29.0.3" defaultConfig { minSdkVersion 21 targetSdkVersion 29 versionCode 1 versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles "consumer-rules.pro" } buildTypes { diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt index f88083f818..7b24f4bb46 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt @@ -16,6 +16,9 @@ package im.vector.riotx.attachment_viewer +import android.content.Context +import android.view.View + sealed class AttachmentInfo { data class Image(val url: String, val data: Any?) : AttachmentInfo() data class AnimatedImage(val url: String, val data: Any?) : AttachmentInfo() @@ -34,5 +37,8 @@ interface AttachmentSourceProvider { fun getAttachmentInfoAt(position: Int): AttachmentInfo fun loadImage(holder: ZoomableImageViewHolder, info: AttachmentInfo.Image) + fun loadImage(holder: AnimatedImageViewHolder, info: AttachmentInfo.AnimatedImage) + + fun overlayViewAtPosition(context: Context, position: Int) : View? } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentViewerActivity.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentViewerActivity.kt index 2d4cbff00d..4d2b4e3459 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentViewerActivity.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentViewerActivity.kt @@ -25,7 +25,9 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat import androidx.core.view.isVisible +import androidx.core.view.updatePadding import androidx.viewpager2.widget.ViewPager2 import kotlinx.android.synthetic.main.activity_attachment_viewer.* import kotlin.math.abs @@ -36,8 +38,16 @@ abstract class AttachmentViewerActivity : AppCompatActivity() { lateinit var imageTransitionView: ImageView lateinit var transitionImageContainer: ViewGroup - // TODO + var topInset = 0 + private var overlayView: View? = null + set(value) { + if (value == overlayView) return + overlayView?.let { rootContainer.removeView(it) } + rootContainer.addView(value) + value?.updatePadding(top = topInset) + field = value + } private lateinit var swipeDismissHandler: SwipeToDismissHandler private lateinit var directionDetector: SwipeDirectionDetector @@ -53,6 +63,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity() { private var wasScaled: Boolean = false private var isSwipeToDismissAllowed: Boolean = true private lateinit var attachmentsAdapter: AttachmentsAdapter + private var isOverlayWasClicked = false // private val shouldDismissToBottom: Boolean // get() = e == null @@ -67,6 +78,20 @@ abstract class AttachmentViewerActivity : AppCompatActivity() { 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) + +// // clear FLAG_TRANSLUCENT_STATUS flag: +// window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); +// +//// add FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS flag to the window +// window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + + setContentView(R.layout.activity_attachment_viewer) attachmentPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL attachmentsAdapter = AttachmentsAdapter() @@ -83,6 +108,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity() { override fun onPageSelected(position: Int) { currentPosition = position + overlayView = attachmentsAdapter.attachmentSourceProvider?.overlayViewAtPosition(this@AttachmentViewerActivity, position) } }) @@ -92,6 +118,13 @@ abstract class AttachmentViewerActivity : AppCompatActivity() { scaleDetector = createScaleGestureDetector() + + ViewCompat.setOnApplyWindowInsetsListener(rootContainer) { _, insets -> + overlayView?.updatePadding(top = insets.systemWindowInsetTop) + topInset = insets.systemWindowInsetTop + insets + } + } override fun dispatchTouchEvent(ev: MotionEvent): Boolean { @@ -99,9 +132,9 @@ abstract class AttachmentViewerActivity : AppCompatActivity() { // The zoomable view is configured to disallow interception when image is zoomed // Check if the overlay is visible, and wants to handle the click -// if (overlayView.isVisible && overlayView?.dispatchTouchEvent(event) == true) { -// return true -// } + if (overlayView?.isVisible == true && overlayView?.dispatchTouchEvent(ev) == true) { + return true + } Log.v("ATTACHEMENTS", "================\ndispatchTouchEvent $ev") @@ -143,14 +176,14 @@ abstract class AttachmentViewerActivity : AppCompatActivity() { attachmentPager.dispatchTouchEvent(event) swipeDismissHandler.onTouch(rootContainer, event) -// isOverlayWasClicked = dispatchOverlayTouch(event) + isOverlayWasClicked = dispatchOverlayTouch(event) } private fun handleEventActionUp(event: MotionEvent) { // wasDoubleTapped = false swipeDismissHandler.onTouch(rootContainer, event) attachmentPager.dispatchTouchEvent(event) -// isOverlayWasClicked = dispatchOverlayTouch(event) + isOverlayWasClicked = dispatchOverlayTouch(event) } private fun handleTouchIfNotScaled(event: MotionEvent): Boolean { @@ -159,7 +192,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity() { directionDetector.handleTouchEvent(event) return when (swipeDirection) { - SwipeDirection.Up, SwipeDirection.Down -> { + SwipeDirection.Up, SwipeDirection.Down -> { if (isSwipeToDismissAllowed && !wasScaled && isImagePagerIdle) { swipeDismissHandler.onTouch(rootContainer, event) } else true @@ -167,7 +200,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity() { SwipeDirection.Left, SwipeDirection.Right -> { attachmentPager.dispatchTouchEvent(event) } - else -> true + else -> true } } diff --git a/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt new file mode 100644 index 0000000000..49930fde76 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt @@ -0,0 +1,58 @@ +/* + * 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.riotx.features.media + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.updateLayoutParams +import im.vector.riotx.R +import im.vector.riotx.attachment_viewer.AttachmentInfo + +class AttachmentOverlayView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + + var onShareCallback: (() -> Unit) ? = null + var onBack: (() -> Unit) ? = null + + private val counterTextView: TextView + private val infoTextView: TextView + private val shareImage: ImageView + + init { + View.inflate(context, R.layout.merge_image_attachment_overlay, this) + setBackgroundColor(Color.TRANSPARENT) + counterTextView = findViewById(R.id.overlayCounterText) + infoTextView = findViewById(R.id.overlayInfoText) + shareImage = findViewById(R.id.overlayShareButton) + + findViewById(R.id.overlayBackButton).setOnClickListener { + onBack?.invoke() + } + } + + fun updateWith(counter: String, senderInfo : String) { + counterTextView.text = counter + infoTextView.text = senderInfo + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt index 079c435001..099d4fed5d 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt @@ -16,7 +16,9 @@ package im.vector.riotx.features.media +import android.content.Context import android.graphics.drawable.Drawable +import android.view.View import com.bumptech.glide.request.target.CustomViewTarget import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.MessageContent @@ -28,14 +30,26 @@ import im.vector.riotx.attachment_viewer.AnimatedImageViewHolder import im.vector.riotx.attachment_viewer.AttachmentInfo import im.vector.riotx.attachment_viewer.AttachmentSourceProvider import im.vector.riotx.attachment_viewer.ZoomableImageViewHolder +import im.vector.riotx.core.date.VectorDateFormatter +import im.vector.riotx.core.extensions.localDateTime import javax.inject.Inject class RoomAttachmentProvider( private val attachments: List, private val initialIndex: Int, - private val imageContentRenderer: ImageContentRenderer + private val imageContentRenderer: ImageContentRenderer, + private val dateFormatter: VectorDateFormatter ) : AttachmentSourceProvider { + interface InteractionListener { + fun onDismissTapped() + fun onShareTapped() + } + + var interactionListener: InteractionListener? = null + + private var overlayView: AttachmentOverlayView? = null + override fun getItemCount(): Int { return attachments.size } @@ -79,6 +93,26 @@ class RoomAttachmentProvider( imageContentRenderer.render(it, holder.touchImageView, holder.customTargetView as CustomViewTarget<*, Drawable>) } } + + override fun overlayViewAtPosition(context: Context, position: Int): View? { + if (overlayView == null) { + overlayView = AttachmentOverlayView(context) + overlayView?.onBack = { + interactionListener?.onDismissTapped() + } + overlayView?.onShareCallback = { + interactionListener?.onShareTapped() + } + } + val item = attachments[position] + val dateString = item.root.localDateTime().let { + "${dateFormatter.formatMessageDay(it)} at ${dateFormatter.formatMessageHour(it)} " + } + overlayView?.updateWith("${position + 1} of ${attachments.size}","${item.senderInfo.displayName} $dateString" ) + return overlayView + } + + // override fun loadImage(holder: ImageViewHolder, info: AttachmentInfo.Image) { // (info.data as? ImageContentRenderer.Data)?.let { // imageContentRenderer.render(it, ImageContentRenderer.Mode.FULL_SIZE, holder.touchImageView) @@ -87,10 +121,11 @@ class RoomAttachmentProvider( } class RoomAttachmentProviderFactory @Inject constructor( - private val imageContentRenderer: ImageContentRenderer + private val imageContentRenderer: ImageContentRenderer, + private val vectorDateFormatter: VectorDateFormatter ) { fun createProvider(attachments: List, initialIndex: Int): RoomAttachmentProvider { - return RoomAttachmentProvider(attachments, initialIndex, imageContentRenderer) + return RoomAttachmentProvider(attachments, initialIndex, imageContentRenderer, vectorDateFormatter) } } diff --git a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt index 2df8bfd0f6..efc9eca517 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt @@ -22,17 +22,15 @@ import android.os.Parcelable import android.view.View import android.view.ViewTreeObserver import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat import androidx.core.transition.addListener import androidx.core.view.ViewCompat import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.transition.Transition +import im.vector.riotx.R import im.vector.riotx.attachment_viewer.AttachmentViewerActivity -import im.vector.riotx.core.di.ActiveSessionHolder -import im.vector.riotx.core.di.DaggerScreenComponent -import im.vector.riotx.core.di.HasVectorInjector -import im.vector.riotx.core.di.ScreenComponent -import im.vector.riotx.core.di.VectorComponent +import im.vector.riotx.core.di.* import im.vector.riotx.features.themes.ActivityOtherThemes import im.vector.riotx.features.themes.ThemeUtils import kotlinx.android.parcel.Parcelize @@ -40,7 +38,7 @@ import timber.log.Timber import javax.inject.Inject import kotlin.system.measureTimeMillis -class VectorAttachmentViewerActivity : AttachmentViewerActivity() { +class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmentProvider.InteractionListener { @Parcelize data class Args( @@ -103,11 +101,15 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity() { } } - setSourceProvider(dataSourceFactory.createProvider(events, index)) + val sourceProvider = dataSourceFactory.createProvider(events, index) + sourceProvider.interactionListener = this + setSourceProvider(sourceProvider) if (savedInstanceState == null) { pager2.setCurrentItem(index, false) } + window.statusBarColor = ContextCompat.getColor(this, R.color.black_alpha) + } private fun getOtherThemes() = ActivityOtherThemes.VectorAttachmentsPreview @@ -204,4 +206,12 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity() { } } + + override fun onDismissTapped() { + animateClose() + } + + override fun onShareTapped() { + TODO("Not yet implemented") + } } diff --git a/vector/src/main/res/layout/merge_image_attachment_overlay.xml b/vector/src/main/res/layout/merge_image_attachment_overlay.xml new file mode 100644 index 0000000000..07d4baedc1 --- /dev/null +++ b/vector/src/main/res/layout/merge_image_attachment_overlay.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/colors_riotx.xml b/vector/src/main/res/values/colors_riotx.xml index a9cb32c3fd..c9d1c2a223 100644 --- a/vector/src/main/res/values/colors_riotx.xml +++ b/vector/src/main/res/values/colors_riotx.xml @@ -40,6 +40,7 @@ #FF000000 #FFFFFFFF + #55000000 Ongoing conference call.\nJoin as %1$s or %2$s Voice From 8c4c909f44c14ff9fd354259f3d7dcd4fb33d48e Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 8 Jul 2020 22:27:00 +0200 Subject: [PATCH 11/41] share action --- .../features/media/AttachmentOverlayView.kt | 3 ++ .../media/VectorAttachmentViewerActivity.kt | 38 ++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt index ebd54bcd0b..05ebe17dea 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt @@ -58,6 +58,9 @@ class AttachmentOverlayView @JvmOverloads constructor( findViewById(R.id.overlayBackButton).setOnClickListener { onBack?.invoke() } + findViewById(R.id.overlayShareButton).setOnClickListener { + onShareCallback?.invoke() + } } fun updateWith(counter: String, senderInfo: String) { diff --git a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt index 4c310b9c47..44b536b2ae 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt @@ -23,11 +23,21 @@ import android.view.View import android.view.ViewTreeObserver import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import androidx.core.net.toUri import androidx.core.transition.addListener import androidx.core.view.ViewCompat import androidx.core.view.isInvisible import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle import androidx.transition.Transition +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.file.FileService +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent +import im.vector.matrix.android.api.session.room.model.message.getFileUrl +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.riotx.R import im.vector.riotx.attachmentviewer.AttachmentViewerActivity import im.vector.riotx.core.di.ActiveSessionHolder @@ -35,10 +45,13 @@ import im.vector.riotx.core.di.DaggerScreenComponent import im.vector.riotx.core.di.HasVectorInjector import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.VectorComponent +import im.vector.riotx.core.intent.getMimeTypeFromUri +import im.vector.riotx.core.utils.shareMedia import im.vector.riotx.features.themes.ActivityOtherThemes import im.vector.riotx.features.themes.ThemeUtils import kotlinx.android.parcel.Parcelize import timber.log.Timber +import java.io.File import javax.inject.Inject import kotlin.system.measureTimeMillis @@ -64,6 +77,7 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen private var initialIndex = 0 private var isAnimatingOut = false + private var eventList: List? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -81,6 +95,7 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen val room = args.roomId?.let { session.getRoom(it) } val events = room?.getAttachmentMessages() ?: emptyList() + eventList = events val index = events.indexOfFirst { it.eventId == args.eventId } initialIndex = index @@ -228,6 +243,27 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen } override fun onShareTapped() { - TODO("Not yet implemented") + // Share + eventList?.get(currentPosition)?.let { timelineEvent -> + + val messageContent = timelineEvent.root.getClearContent().toModel() + as? MessageWithAttachmentContent + ?: return@let + sessionHolder.getSafeActiveSession()?.fileService()?.downloadFile( + downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, + id = timelineEvent.eventId, + fileName = messageContent.body, + mimeType = messageContent.mimeType, + url = messageContent.getFileUrl(), + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), + callback = object : MatrixCallback { + override fun onSuccess(data: File) { + if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { + shareMedia(this@VectorAttachmentViewerActivity, data, getMimeTypeFromUri(this@VectorAttachmentViewerActivity, data.toUri())) + } + } + } + ) + } } } From e9778d6febdc57a516fd58e2b1e35bf2a00a227c Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 8 Jul 2020 22:41:17 +0200 Subject: [PATCH 12/41] Video stop/resume when paging or bg/fg --- .../AttachmentViewerActivity.kt | 9 +++++++ .../attachmentviewer/AttachmentsAdapter.kt | 13 ++++++++++ .../riotx/attachmentviewer/VideoViewHolder.kt | 25 ++++++++++++++++++- 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt index 99a90eb033..2a83ab21c7 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt @@ -143,6 +143,15 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi overlayView = attachmentsAdapter.attachmentSourceProvider?.overlayViewAtPosition(this@AttachmentViewerActivity, position) } + override fun onPause() { + attachmentsAdapter.onPause(currentPosition) + super.onPause() + } + + override fun onResume() { + super.onResume() + attachmentsAdapter.onResume(currentPosition) + } override fun dispatchTouchEvent(ev: MotionEvent): Boolean { // The zoomable view is configured to disallow interception when image is zoomed diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt index 333a1b3625..d1929f271e 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt @@ -28,6 +28,8 @@ abstract class BaseViewHolder constructor(itemView: View) : open fun onRecycled() {} open fun onAttached() {} open fun onDetached() {} + open fun entersBackground() {} + open fun entersForeground() {} open fun onSelected(selected: Boolean) {} } @@ -121,6 +123,17 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { return false } + + fun onPause(position: Int) { + val holder = recyclerView?.findViewHolderForAdapterPosition(position) as? BaseViewHolder + holder?.entersBackground() + } + + fun onResume(position: Int) { + val holder = recyclerView?.findViewHolderForAdapterPosition(position) as? BaseViewHolder + holder?.entersForeground() + + } // override fun getItemCount(): Int { // return 8 // } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt index 38b656559e..ea5fed1acb 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt @@ -26,6 +26,7 @@ import androidx.core.view.isVisible import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable +import kotlinx.coroutines.selects.select import java.io.File import java.lang.ref.WeakReference import java.util.concurrent.TimeUnit @@ -39,6 +40,7 @@ class VideoViewHolder constructor(itemView: View) : private var isSelected = false private var mVideoPath: String? = null private var progressDisposable: Disposable? = null + private var progress: Int = 0 var eventListener: WeakReference? = null @@ -89,12 +91,30 @@ class VideoViewHolder constructor(itemView: View) : } } + override fun entersBackground() { + if (videoView.isPlaying) { + progress = videoView.currentPosition + progressDisposable?.dispose() + progressDisposable = null + videoView.stopPlayback() + videoView.pause() + } + + } + + override fun entersForeground() { + onSelected(isSelected) + } + override fun onSelected(selected: Boolean) { if (!selected) { if (videoView.isPlaying) { + progress = videoView.currentPosition videoView.stopPlayback() progressDisposable?.dispose() progressDisposable = null + } else { + progress = 0 } } else { if (mVideoPath != null) { @@ -125,9 +145,12 @@ class VideoViewHolder constructor(itemView: View) : videoView.setVideoPath(mVideoPath) videoView.start() + if (progress > 0) { + videoView.seekTo(progress) + } } override fun bind(attachmentInfo: AttachmentInfo) { - Log.v("FOO", "") + progress = 0 } } From e24d5b3ca403ea42f72992c15c5dda89ff135dbe Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 8 Jul 2020 22:58:27 +0200 Subject: [PATCH 13/41] Simple play/pause overlay --- .../attachmentviewer/AttachmentEvents.kt | 6 +++- .../AttachmentViewerActivity.kt | 7 +++++ .../attachmentviewer/AttachmentsAdapter.kt | 4 +-- .../riotx/attachmentviewer/VideoViewHolder.kt | 30 ++++++++++++++----- .../features/media/AttachmentOverlayView.kt | 7 +++++ .../features/media/RoomAttachmentProvider.kt | 4 +++ .../media/VectorAttachmentViewerActivity.kt | 5 ++++ 7 files changed, 53 insertions(+), 10 deletions(-) diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentEvents.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentEvents.kt index 997790a938..5b1f2ab90d 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentEvents.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentEvents.kt @@ -21,6 +21,10 @@ sealed class AttachmentEvents { } interface AttachmentEventListener { - fun onEvent(event: AttachmentEvents) } + +sealed class AttachmentCommands { + object PauseVideo : AttachmentCommands() + object StartVideo : AttachmentCommands() +} diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt index 2a83ab21c7..6f2436f261 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt @@ -152,6 +152,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi super.onResume() attachmentsAdapter.onResume(currentPosition) } + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { // The zoomable view is configured to disallow interception when image is zoomed @@ -302,6 +303,12 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi finish() } + public fun handle(commands: AttachmentCommands) { + (attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition) as? BaseViewHolder)?.let { + it.handleCommand(commands) + } + } + private fun hideSystemUI() { systemUiVisibility = false // Enables regular immersive mode. diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt index d1929f271e..b0cb5193e8 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt @@ -31,6 +31,8 @@ abstract class BaseViewHolder constructor(itemView: View) : open fun entersBackground() {} open fun entersForeground() {} open fun onSelected(selected: Boolean) {} + + open fun handleCommand(commands: AttachmentCommands) {} } class AttachmentViewHolder constructor(itemView: View) : @@ -123,7 +125,6 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { return false } - fun onPause(position: Int) { val holder = recyclerView?.findViewHolderForAdapterPosition(position) as? BaseViewHolder holder?.entersBackground() @@ -132,7 +133,6 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { fun onResume(position: Int) { val holder = recyclerView?.findViewHolderForAdapterPosition(position) as? BaseViewHolder holder?.entersForeground() - } // override fun getItemCount(): Int { // return 8 diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt index ea5fed1acb..a2424dda57 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt @@ -26,7 +26,6 @@ import androidx.core.view.isVisible import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable -import kotlinx.coroutines.selects.select import java.io.File import java.lang.ref.WeakReference import java.util.concurrent.TimeUnit @@ -41,6 +40,7 @@ class VideoViewHolder constructor(itemView: View) : private var mVideoPath: String? = null private var progressDisposable: Disposable? = null private var progress: Int = 0 + private var wasPaused = false var eventListener: WeakReference? = null @@ -99,7 +99,6 @@ class VideoViewHolder constructor(itemView: View) : videoView.stopPlayback() videoView.pause() } - } override fun entersForeground() { @@ -111,11 +110,11 @@ class VideoViewHolder constructor(itemView: View) : if (videoView.isPlaying) { progress = videoView.currentPosition videoView.stopPlayback() - progressDisposable?.dispose() - progressDisposable = null } else { progress = 0 } + progressDisposable?.dispose() + progressDisposable = null } else { if (mVideoPath != null) { startPlaying() @@ -144,13 +143,30 @@ class VideoViewHolder constructor(itemView: View) : } videoView.setVideoPath(mVideoPath) - videoView.start() - if (progress > 0) { - videoView.seekTo(progress) + if (!wasPaused) { + videoView.start() + if (progress > 0) { + videoView.seekTo(progress) + } + } + } + + override fun handleCommand(commands: AttachmentCommands) { + if (!isSelected) return + when (commands) { + AttachmentCommands.StartVideo -> { + wasPaused = false + videoView.start() + } + AttachmentCommands.PauseVideo -> { + wasPaused = true + videoView.pause() + } } } override fun bind(attachmentInfo: AttachmentInfo) { progress = 0 + wasPaused = false } } diff --git a/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt index 05ebe17dea..a2657f7daf 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt @@ -35,6 +35,7 @@ class AttachmentOverlayView @JvmOverloads constructor( var onShareCallback: (() -> Unit)? = null var onBack: (() -> Unit)? = null + var onPlayPause: ((play: Boolean) -> Unit)? = null private val counterTextView: TextView private val infoTextView: TextView @@ -42,6 +43,8 @@ class AttachmentOverlayView @JvmOverloads constructor( private val overlayPlayPauseButton: ImageView private val overlaySeekBar: SeekBar + var isPlaying = false + val videoControlsGroup: Group init { @@ -61,6 +64,9 @@ class AttachmentOverlayView @JvmOverloads constructor( findViewById(R.id.overlayShareButton).setOnClickListener { onShareCallback?.invoke() } + findViewById(R.id.overlayPlayPauseButton).setOnClickListener { + onPlayPause?.invoke(!isPlaying) + } } fun updateWith(counter: String, senderInfo: String) { @@ -74,6 +80,7 @@ class AttachmentOverlayView @JvmOverloads constructor( overlayPlayPauseButton.setImageResource(if (!event.isPlaying) R.drawable.ic_play_arrow else R.drawable.ic_pause) val safeDuration = (if (event.duration == 0) 100 else event.duration).toFloat() val percent = ((event.progress / safeDuration) * 100f).toInt().coerceAtMost(100) + isPlaying = event.isPlaying overlaySeekBar.progress = percent } } diff --git a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt index 09459b20d1..9f6080d95f 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt @@ -57,6 +57,7 @@ class RoomAttachmentProvider( interface InteractionListener { fun onDismissTapped() fun onShareTapped() + fun onPlayPause(play: Boolean) } var interactionListener: InteractionListener? = null @@ -197,6 +198,9 @@ class RoomAttachmentProvider( overlayView?.onShareCallback = { interactionListener?.onShareTapped() } + overlayView?.onPlayPause = { play -> + interactionListener?.onPlayPause(play) + } } val item = attachments[position] val dateString = item.root.localDateTime().let { diff --git a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt index 44b536b2ae..2606f0bb76 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt @@ -39,6 +39,7 @@ import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.riotx.R +import im.vector.riotx.attachmentviewer.AttachmentCommands import im.vector.riotx.attachmentviewer.AttachmentViewerActivity import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.DaggerScreenComponent @@ -242,6 +243,10 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen animateClose() } + override fun onPlayPause(play: Boolean) { + handle(if (play) AttachmentCommands.StartVideo else AttachmentCommands.PauseVideo) + } + override fun onShareTapped() { // Share eventList?.get(currentPosition)?.let { timelineEvent -> From bf2d937ad62556232b8e8ab6bf5fdd6e6e52de02 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 9 Jul 2020 08:59:06 +0200 Subject: [PATCH 14/41] Basic video seekTo support --- .../attachmentviewer/AttachmentEvents.kt | 1 + .../AttachmentViewerActivity.kt | 7 ++--- .../riotx/attachmentviewer/VideoViewHolder.kt | 7 +++++ .../features/media/AttachmentOverlayView.kt | 31 +++++++++++++++---- .../features/media/RoomAttachmentProvider.kt | 4 +++ .../media/VectorAttachmentViewerActivity.kt | 4 +++ 6 files changed, 44 insertions(+), 10 deletions(-) diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentEvents.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentEvents.kt index 5b1f2ab90d..b2b6c9fe16 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentEvents.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentEvents.kt @@ -27,4 +27,5 @@ interface AttachmentEventListener { sealed class AttachmentCommands { object PauseVideo : AttachmentCommands() object StartVideo : AttachmentCommands() + data class SeekTo(val percentProgress: Int) : AttachmentCommands() } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt index 6f2436f261..029064e058 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt @@ -303,10 +303,9 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi finish() } - public fun handle(commands: AttachmentCommands) { - (attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition) as? BaseViewHolder)?.let { - it.handleCommand(commands) - } + fun handle(commands: AttachmentCommands) { + (attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition) as? BaseViewHolder) + ?.handleCommand(commands) } private fun hideSystemUI() { diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt index a2424dda57..5718147bab 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt @@ -162,6 +162,13 @@ class VideoViewHolder constructor(itemView: View) : wasPaused = true videoView.pause() } + is AttachmentCommands.SeekTo -> { + val duration = videoView.duration + if (duration > 0) { + val seekDuration = duration * (commands.percentProgress / 100f) + videoView.seekTo(seekDuration.toInt()) + } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt index a2657f7daf..2812b011f9 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/AttachmentOverlayView.kt @@ -36,6 +36,7 @@ class AttachmentOverlayView @JvmOverloads constructor( var onShareCallback: (() -> Unit)? = null var onBack: (() -> Unit)? = null var onPlayPause: ((play: Boolean) -> Unit)? = null + var videoSeekTo: ((progress: Int) -> Unit)? = null private val counterTextView: TextView private val infoTextView: TextView @@ -47,6 +48,8 @@ class AttachmentOverlayView @JvmOverloads constructor( val videoControlsGroup: Group + var suspendSeekBarUpdate = false + init { View.inflate(context, R.layout.merge_image_attachment_overlay, this) setBackgroundColor(Color.TRANSPARENT) @@ -56,8 +59,6 @@ class AttachmentOverlayView @JvmOverloads constructor( videoControlsGroup = findViewById(R.id.overlayVideoControlsGroup) overlayPlayPauseButton = findViewById(R.id.overlayPlayPauseButton) overlaySeekBar = findViewById(R.id.overlaySeekBar) - - overlaySeekBar.isEnabled = false findViewById(R.id.overlayBackButton).setOnClickListener { onBack?.invoke() } @@ -67,6 +68,22 @@ class AttachmentOverlayView @JvmOverloads constructor( findViewById(R.id.overlayPlayPauseButton).setOnClickListener { onPlayPause?.invoke(!isPlaying) } + + overlaySeekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { + if (fromUser) { + videoSeekTo?.invoke(progress) + } + } + + override fun onStartTrackingTouch(seekBar: SeekBar?) { + suspendSeekBarUpdate = true + } + + override fun onStopTrackingTouch(seekBar: SeekBar?) { + suspendSeekBarUpdate = false + } + }) } fun updateWith(counter: String, senderInfo: String) { @@ -78,10 +95,12 @@ class AttachmentOverlayView @JvmOverloads constructor( when (event) { is AttachmentEvents.VideoEvent -> { overlayPlayPauseButton.setImageResource(if (!event.isPlaying) R.drawable.ic_play_arrow else R.drawable.ic_pause) - val safeDuration = (if (event.duration == 0) 100 else event.duration).toFloat() - val percent = ((event.progress / safeDuration) * 100f).toInt().coerceAtMost(100) - isPlaying = event.isPlaying - overlaySeekBar.progress = percent + if (!suspendSeekBarUpdate) { + val safeDuration = (if (event.duration == 0) 100 else event.duration).toFloat() + val percent = ((event.progress / safeDuration) * 100f).toInt().coerceAtMost(100) + isPlaying = event.isPlaying + overlaySeekBar.progress = percent + } } } } diff --git a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt index 9f6080d95f..4e30e0179a 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt @@ -58,6 +58,7 @@ class RoomAttachmentProvider( fun onDismissTapped() fun onShareTapped() fun onPlayPause(play: Boolean) + fun videoSeekTo(percent: Int) } var interactionListener: InteractionListener? = null @@ -201,6 +202,9 @@ class RoomAttachmentProvider( overlayView?.onPlayPause = { play -> interactionListener?.onPlayPause(play) } + overlayView?.videoSeekTo = { percent -> + interactionListener?.videoSeekTo(percent) + } } val item = attachments[position] val dateString = item.root.localDateTime().let { diff --git a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt index 2606f0bb76..10483f3fa9 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt @@ -247,6 +247,10 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen handle(if (play) AttachmentCommands.StartVideo else AttachmentCommands.PauseVideo) } + override fun videoSeekTo(percent: Int) { + handle(AttachmentCommands.SeekTo(percent)) + } + override fun onShareTapped() { // Share eventList?.get(currentPosition)?.let { timelineEvent -> From aa3e68f3fd433099e10c1ef8f4818e3ac48ba9b6 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 9 Jul 2020 10:08:55 +0200 Subject: [PATCH 15/41] Refactoring Remove glide dependency + protect against cell reuse bugs --- attachment-viewer/build.gradle | 2 - .../AnimatedImageViewHolder.kt | 38 +------ .../AttachmentSourceProvider.kt | 25 ++--- .../attachmentviewer/AttachmentsAdapter.kt | 75 ++++--------- .../attachmentviewer/ImageLoaderTarget.kt | 103 ++++++++++++++++++ .../attachmentviewer/VideoLoaderTarget.kt | 76 +++++++++++++ .../riotx/attachmentviewer/VideoViewHolder.kt | 33 +----- .../ZoomableImageViewHolder.kt | 34 +----- .../features/media/RoomAttachmentProvider.kt | 82 +++++++++----- 9 files changed, 279 insertions(+), 189 deletions(-) create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ImageLoaderTarget.kt create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoLoaderTarget.kt diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle index 6b64e661fa..3a5c3298d4 100644 --- a/attachment-viewer/build.gradle +++ b/attachment-viewer/build.gradle @@ -58,9 +58,7 @@ android { } dependencies { -// implementation 'com.github.MikeOrtiz:TouchImageView:3.0.2' implementation 'com.github.chrisbanes:PhotoView:2.0.0' - implementation "com.github.bumptech.glide:glide:4.10.0" implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AnimatedImageViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AnimatedImageViewHolder.kt index 9f512e78be..f00a4eff30 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AnimatedImageViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AnimatedImageViewHolder.kt @@ -16,16 +16,9 @@ package im.vector.riotx.attachmentviewer -import android.graphics.drawable.Animatable -import android.graphics.drawable.Drawable import android.view.View import android.widget.ImageView -import android.widget.LinearLayout import android.widget.ProgressBar -import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams -import com.bumptech.glide.request.target.CustomViewTarget -import com.bumptech.glide.request.transition.Transition class AnimatedImageViewHolder constructor(itemView: View) : BaseViewHolder(itemView) { @@ -33,34 +26,5 @@ class AnimatedImageViewHolder constructor(itemView: View) : val touchImageView: ImageView = itemView.findViewById(R.id.imageView) val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress) - val customTargetView = object : CustomViewTarget(touchImageView) { - - override fun onResourceLoading(placeholder: Drawable?) { - imageLoaderProgress.isVisible = true - } - - override fun onLoadFailed(errorDrawable: Drawable?) { - imageLoaderProgress.isVisible = false - } - - override fun onResourceCleared(placeholder: Drawable?) { - touchImageView.setImageDrawable(placeholder) - } - - override fun onResourceReady(resource: Drawable, transition: Transition?) { - imageLoaderProgress.isVisible = false - // Glide mess up the view size :/ - touchImageView.updateLayoutParams { - width = LinearLayout.LayoutParams.MATCH_PARENT - height = LinearLayout.LayoutParams.MATCH_PARENT - } - touchImageView.setImageDrawable(resource) - if (resource is Animatable) { - resource.start() - } - } - } - - override fun bind(attachmentInfo: AttachmentInfo) { - } + internal val target = DefaultImageLoaderTarget(this, this.touchImageView) } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentSourceProvider.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentSourceProvider.kt index 930fc62658..ce725afec2 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentSourceProvider.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentSourceProvider.kt @@ -19,15 +19,12 @@ package im.vector.riotx.attachmentviewer import android.content.Context import android.view.View -sealed class AttachmentInfo { - data class Image(val url: String, val data: Any?) : AttachmentInfo() - data class AnimatedImage(val url: String, val data: Any?) : AttachmentInfo() - data class Video(val url: String, val data: Any, val thumbnail: Image?) : AttachmentInfo() - data class Audio(val url: String, val data: Any) : AttachmentInfo() - data class File(val url: String, val data: Any) : AttachmentInfo() - - fun bind() { - } +sealed class AttachmentInfo(open val uid: String) { + data class Image(override val uid: String, val url: String, val data: Any?) : AttachmentInfo(uid) + data class AnimatedImage(override val uid: String, val url: String, val data: Any?) : AttachmentInfo(uid) + data class Video(override val uid: String, val url: String, val data: Any, val thumbnail: Image?) : AttachmentInfo(uid) + data class Audio(override val uid: String, val url: String, val data: Any) : AttachmentInfo(uid) + data class File(override val uid: String, val url: String, val data: Any) : AttachmentInfo(uid) } interface AttachmentSourceProvider { @@ -36,11 +33,13 @@ interface AttachmentSourceProvider { fun getAttachmentInfoAt(position: Int): AttachmentInfo - fun loadImage(holder: ZoomableImageViewHolder, info: AttachmentInfo.Image) + fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.Image) - fun loadImage(holder: AnimatedImageViewHolder, info: AttachmentInfo.AnimatedImage) + fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.AnimatedImage) - fun loadVideo(holder: VideoViewHolder, info: AttachmentInfo.Video) + fun loadVideo(target: VideoLoaderTarget, info: AttachmentInfo.Video) - fun overlayViewAtPosition(context: Context, position: Int) : View? + fun overlayViewAtPosition(context: Context, position: Int): View? + + fun clear(id: String) } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt index b0cb5193e8..2f453b58a8 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt @@ -24,8 +24,10 @@ import androidx.recyclerview.widget.RecyclerView abstract class BaseViewHolder constructor(itemView: View) : RecyclerView.ViewHolder(itemView) { - open fun bind(attachmentInfo: AttachmentInfo) {} - open fun onRecycled() {} + open fun onRecycled() { + boundResourceUid = null + } + open fun onAttached() {} open fun onDetached() {} open fun entersBackground() {} @@ -33,16 +35,17 @@ abstract class BaseViewHolder constructor(itemView: View) : open fun onSelected(selected: Boolean) {} open fun handleCommand(commands: AttachmentCommands) {} -} -class AttachmentViewHolder constructor(itemView: View) : - BaseViewHolder(itemView) { + var boundResourceUid: String? = null - override fun bind(attachmentInfo: AttachmentInfo) { + open fun bind(attachmentInfo: AttachmentInfo) { + boundResourceUid = attachmentInfo.uid } } -// class AttachmentsAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : FragmentStateAdapter(fragmentManager, lifecycle) { +class AttachmentViewHolder constructor(itemView: View) : + BaseViewHolder(itemView) + class AttachmentsAdapter() : RecyclerView.Adapter() { var attachmentSourceProvider: AttachmentSourceProvider? = null @@ -65,21 +68,21 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { val inflater = LayoutInflater.from(parent.context) val itemView = inflater.inflate(viewType, parent, false) return when (viewType) { - R.layout.item_image_attachment -> ZoomableImageViewHolder(itemView) + R.layout.item_image_attachment -> ZoomableImageViewHolder(itemView) R.layout.item_animated_image_attachment -> AnimatedImageViewHolder(itemView) - R.layout.item_video_attachment -> VideoViewHolder(itemView) - else -> AttachmentViewHolder(itemView) + R.layout.item_video_attachment -> VideoViewHolder(itemView) + else -> AttachmentViewHolder(itemView) } } override fun getItemViewType(position: Int): Int { val info = attachmentSourceProvider!!.getAttachmentInfoAt(position) return when (info) { - is AttachmentInfo.Image -> R.layout.item_image_attachment - is AttachmentInfo.Video -> R.layout.item_video_attachment + is AttachmentInfo.Image -> R.layout.item_image_attachment + is AttachmentInfo.Video -> R.layout.item_video_attachment is AttachmentInfo.AnimatedImage -> R.layout.item_animated_image_attachment - is AttachmentInfo.Audio -> TODO() - is AttachmentInfo.File -> TODO() + is AttachmentInfo.Audio -> TODO() + is AttachmentInfo.File -> TODO() } } @@ -91,16 +94,17 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { attachmentSourceProvider?.getAttachmentInfoAt(position)?.let { holder.bind(it) when (it) { - is AttachmentInfo.Image -> { - attachmentSourceProvider?.loadImage(holder as ZoomableImageViewHolder, it) + is AttachmentInfo.Image -> { + attachmentSourceProvider?.loadImage((holder as ZoomableImageViewHolder).target, it) } is AttachmentInfo.AnimatedImage -> { - attachmentSourceProvider?.loadImage(holder as AnimatedImageViewHolder, it) + attachmentSourceProvider?.loadImage((holder as AnimatedImageViewHolder).target, it) } - is AttachmentInfo.Video -> { - attachmentSourceProvider?.loadVideo(holder as VideoViewHolder, it) + is AttachmentInfo.Video -> { + attachmentSourceProvider?.loadVideo((holder as VideoViewHolder).target, it) + } + else -> { } - else -> {} } } } @@ -134,35 +138,4 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { val holder = recyclerView?.findViewHolderForAdapterPosition(position) as? BaseViewHolder holder?.entersForeground() } -// override fun getItemCount(): Int { -// return 8 -// } -// -// override fun createFragment(position: Int): Fragment { -// // Return a NEW fragment instance in createFragment(int) -// val fragment = DemoObjectFragment() -// fragment.arguments = Bundle().apply { -// // Our object is just an integer :-P -// putInt(ARG_OBJECT, position + 1) -// } -// return fragment -// } } - -// private const val ARG_OBJECT = "object" -// -// // Instances of this class are fragments representing a single -// // object in our collection. -// class DemoObjectFragment : Fragment() { -// -// override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { -// return inflater.inflate(R.layout.view_image_attachment, container, false) -// } -// -// override fun onViewCreated(view: View, savedInstanceState: Bundle?) { -// arguments?.takeIf { it.containsKey(ARG_OBJECT) }?.apply { -// val textView: TextView = view.findViewById(R.id.testPage) -// textView.text = getInt(ARG_OBJECT).toString() -// } -// } -// } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ImageLoaderTarget.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ImageLoaderTarget.kt new file mode 100644 index 0000000000..bb59c9e01e --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ImageLoaderTarget.kt @@ -0,0 +1,103 @@ +/* + * 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.riotx.attachmentviewer + +import android.graphics.drawable.Animatable +import android.graphics.drawable.Drawable +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams + +interface ImageLoaderTarget { + + fun contextView(): ImageView + + fun onResourceLoading(uid: String, placeholder: Drawable?) + + fun onLoadFailed(uid: String, errorDrawable: Drawable?) + + fun onResourceCleared(uid: String, placeholder: Drawable?) + + fun onResourceReady(uid: String, resource: Drawable) +} + +internal class DefaultImageLoaderTarget(val holder: AnimatedImageViewHolder, private val contextView: ImageView) + : ImageLoaderTarget { + override fun contextView(): ImageView { + return contextView + } + + override fun onResourceLoading(uid: String, placeholder: Drawable?) { + if (holder.boundResourceUid != uid) return + holder.imageLoaderProgress.isVisible = true + } + + override fun onLoadFailed(uid: String, errorDrawable: Drawable?) { + if (holder.boundResourceUid != uid) return + holder.imageLoaderProgress.isVisible = false + } + + override fun onResourceCleared(uid: String, placeholder: Drawable?) { + if (holder.boundResourceUid != uid) return + holder.touchImageView.setImageDrawable(placeholder) + } + + override fun onResourceReady(uid: String, resource: Drawable) { + if (holder.boundResourceUid != uid) return + holder.imageLoaderProgress.isVisible = false + // Glide mess up the view size :/ + holder.touchImageView.updateLayoutParams { + width = LinearLayout.LayoutParams.MATCH_PARENT + height = LinearLayout.LayoutParams.MATCH_PARENT + } + holder.touchImageView.setImageDrawable(resource) + if (resource is Animatable) { + resource.start() + } + } + + internal class ZoomableImageTarget(val holder: ZoomableImageViewHolder, private val contextView: ImageView) : ImageLoaderTarget { + override fun contextView() = contextView + + override fun onResourceLoading(uid: String, placeholder: Drawable?) { + if (holder.boundResourceUid != uid) return + holder.imageLoaderProgress.isVisible = true + } + + override fun onLoadFailed(uid: String, errorDrawable: Drawable?) { + if (holder.boundResourceUid != uid) return + holder.imageLoaderProgress.isVisible = false + } + + override fun onResourceCleared(uid: String, placeholder: Drawable?) { + if (holder.boundResourceUid != uid) return + holder.touchImageView.setImageDrawable(placeholder) + } + + override fun onResourceReady(uid: String, resource: Drawable) { + if (holder.boundResourceUid != uid) return + holder.imageLoaderProgress.isVisible = false + // Glide mess up the view size :/ + holder.touchImageView.updateLayoutParams { + width = LinearLayout.LayoutParams.MATCH_PARENT + height = LinearLayout.LayoutParams.MATCH_PARENT + } + holder.touchImageView.setImageDrawable(resource) + } + } +} diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoLoaderTarget.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoLoaderTarget.kt new file mode 100644 index 0000000000..548c6431e5 --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoLoaderTarget.kt @@ -0,0 +1,76 @@ +/* + * 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.riotx.attachmentviewer + +import android.graphics.drawable.Drawable +import android.widget.ImageView +import androidx.core.view.isVisible +import java.io.File + +interface VideoLoaderTarget { + fun contextView(): ImageView + + fun onThumbnailResourceLoading(uid: String, placeholder: Drawable?) + + fun onThumbnailLoadFailed(uid: String, errorDrawable: Drawable?) + + fun onThumbnailResourceCleared(uid: String, placeholder: Drawable?) + + fun onThumbnailResourceReady(uid: String, resource: Drawable) + + fun onVideoFileLoading(uid: String) + fun onVideoFileLoadFailed(uid: String) + fun onVideoFileReady(uid: String, file: File) +} + +internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val contextView: ImageView) : VideoLoaderTarget { + override fun contextView(): ImageView = contextView + + override fun onThumbnailResourceLoading(uid: String, placeholder: Drawable?) { + } + + override fun onThumbnailLoadFailed(uid: String, errorDrawable: Drawable?) { + } + + override fun onThumbnailResourceCleared(uid: String, placeholder: Drawable?) { + } + + override fun onThumbnailResourceReady(uid: String, resource: Drawable) { + if (holder.boundResourceUid != uid) return + holder.thumbnailImage.setImageDrawable(resource) + } + + override fun onVideoFileLoading(uid: String) { + if (holder.boundResourceUid != uid) return + holder.thumbnailImage.isVisible = true + holder.loaderProgressBar.isVisible = true + holder.videoView.isVisible = false + } + + override fun onVideoFileLoadFailed(uid: String) { + if (holder.boundResourceUid != uid) return + holder.videoFileLoadError() + } + + override fun onVideoFileReady(uid: String, file: File) { + if (holder.boundResourceUid != uid) return + holder.thumbnailImage.isVisible = false + holder.loaderProgressBar.isVisible = false + holder.videoView.isVisible = true + holder.videoReady(file) + } +} diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt index 5718147bab..2b417baecc 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt @@ -44,38 +44,13 @@ class VideoViewHolder constructor(itemView: View) : var eventListener: WeakReference? = null -// interface Target { -// fun onResourceLoading(progress: Int, total: Int) -// fun onLoadFailed() -// fun onResourceReady(file: File) -// fun onThumbnailReady(thumbnail: Drawable?) -// } - - init { - } - val thumbnailImage: ImageView = itemView.findViewById(R.id.videoThumbnailImage) val videoView: VideoView = itemView.findViewById(R.id.videoView) val loaderProgressBar: ProgressBar = itemView.findViewById(R.id.videoLoaderProgress) val videoControlIcon: ImageView = itemView.findViewById(R.id.videoControlIcon) val errorTextView: TextView = itemView.findViewById(R.id.videoMediaViewerErrorView) -// val videoTarget = object : Target { -// override fun onResourceLoading(progress: Int, total: Int) { -// videoView.isVisible = false -// loaderProgressBar.isVisible = true -// } -// -// override fun onLoadFailed() { -// loaderProgressBar.isVisible = false -// } -// -// override fun onResourceReady(file: File) { -// } -// -// override fun onThumbnailReady(thumbnail: Drawable?) { -// } -// } + internal val target = DefaultVideoLoaderTarget(this, thumbnailImage) override fun onRecycled() { super.onRecycled() @@ -91,6 +66,9 @@ class VideoViewHolder constructor(itemView: View) : } } + fun videoFileLoadError() { + } + override fun entersBackground() { if (videoView.isPlaying) { progress = videoView.currentPosition @@ -162,7 +140,7 @@ class VideoViewHolder constructor(itemView: View) : wasPaused = true videoView.pause() } - is AttachmentCommands.SeekTo -> { + is AttachmentCommands.SeekTo -> { val duration = videoView.duration if (duration > 0) { val seekDuration = duration * (commands.percentProgress / 100f) @@ -173,6 +151,7 @@ class VideoViewHolder constructor(itemView: View) : } override fun bind(attachmentInfo: AttachmentInfo) { + super.bind(attachmentInfo) progress = 0 wasPaused = false } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ZoomableImageViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ZoomableImageViewHolder.kt index 00a8ad275a..aeaf612bbc 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ZoomableImageViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ZoomableImageViewHolder.kt @@ -16,15 +16,9 @@ package im.vector.riotx.attachmentviewer -import android.graphics.drawable.Drawable import android.util.Log import android.view.View -import android.widget.LinearLayout import android.widget.ProgressBar -import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams -import com.bumptech.glide.request.target.CustomViewTarget -import com.bumptech.glide.request.transition.Transition import com.github.chrisbanes.photoview.PhotoView class ZoomableImageViewHolder constructor(itemView: View) : @@ -45,31 +39,5 @@ class ZoomableImageViewHolder constructor(itemView: View) : touchImageView.setAllowParentInterceptOnEdge(true) } - val customTargetView = object : CustomViewTarget(touchImageView) { - - override fun onResourceLoading(placeholder: Drawable?) { - imageLoaderProgress.isVisible = true - } - - override fun onLoadFailed(errorDrawable: Drawable?) { - imageLoaderProgress.isVisible = false - } - - override fun onResourceCleared(placeholder: Drawable?) { - touchImageView.setImageDrawable(placeholder) - } - - override fun onResourceReady(resource: Drawable, transition: Transition?) { - imageLoaderProgress.isVisible = false - // Glide mess up the view size :/ - touchImageView.updateLayoutParams { - width = LinearLayout.LayoutParams.MATCH_PARENT - height = LinearLayout.LayoutParams.MATCH_PARENT - } - touchImageView.setImageDrawable(resource) - } - } - - override fun bind(attachmentInfo: AttachmentInfo) { - } + internal val target = DefaultImageLoaderTarget.ZoomableImageTarget(this, touchImageView) } diff --git a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt index 4e30e0179a..f7299bf714 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt @@ -35,11 +35,10 @@ import im.vector.matrix.android.api.session.room.model.message.MessageWithAttach import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt -import im.vector.riotx.attachmentviewer.AnimatedImageViewHolder import im.vector.riotx.attachmentviewer.AttachmentInfo import im.vector.riotx.attachmentviewer.AttachmentSourceProvider -import im.vector.riotx.attachmentviewer.VideoViewHolder -import im.vector.riotx.attachmentviewer.ZoomableImageViewHolder +import im.vector.riotx.attachmentviewer.ImageLoaderTarget +import im.vector.riotx.attachmentviewer.VideoLoaderTarget import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.extensions.localDateTime import java.io.File @@ -86,13 +85,15 @@ class RoomAttachmentProvider( ) if (content.mimeType == "image/gif") { AttachmentInfo.AnimatedImage( - content.url ?: "", - data + uid = it.eventId, + url = content.url ?: "", + data = data ) } else { AttachmentInfo.Image( - content.url ?: "", - data + uid = it.eventId, + url = content.url ?: "", + data = data ) } } else if (content is MessageVideoContent) { @@ -117,9 +118,11 @@ class RoomAttachmentProvider( thumbnailMediaData = thumbnailData ) AttachmentInfo.Video( - content.getFileUrl() ?: "", - data, - AttachmentInfo.Image( + uid = it.eventId, + url = content.getFileUrl() ?: "", + data = data, + thumbnail = AttachmentInfo.Image( + uid = it.eventId, url = content.videoInfo?.thumbnailFile?.url ?: content.videoInfo?.thumbnailUrl ?: "", data = thumbnailData @@ -128,49 +131,72 @@ class RoomAttachmentProvider( ) } else { AttachmentInfo.Image( - "", - null + uid = it.eventId, + url = "", + data = null ) } } } - override fun loadImage(holder: ZoomableImageViewHolder, info: AttachmentInfo.Image) { + override fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.Image) { (info.data as? ImageContentRenderer.Data)?.let { - imageContentRenderer.render(it, holder.touchImageView, holder.customTargetView as CustomViewTarget<*, Drawable>) + imageContentRenderer.render(it, target.contextView(), object : CustomViewTarget(target.contextView()) { + override fun onLoadFailed(errorDrawable: Drawable?) { + target.onLoadFailed(info.uid, errorDrawable) + } + + override fun onResourceCleared(placeholder: Drawable?) { + target.onResourceCleared(info.uid, placeholder) + } + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + target.onResourceReady(info.uid, resource) + } + }) } } - override fun loadImage(holder: AnimatedImageViewHolder, info: AttachmentInfo.AnimatedImage) { + override fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.AnimatedImage) { (info.data as? ImageContentRenderer.Data)?.let { - imageContentRenderer.render(it, holder.touchImageView, holder.customTargetView as CustomViewTarget<*, Drawable>) + imageContentRenderer.render(it, target.contextView(), object : CustomViewTarget(target.contextView()) { + override fun onLoadFailed(errorDrawable: Drawable?) { + target.onLoadFailed(info.uid, errorDrawable) + } + + override fun onResourceCleared(placeholder: Drawable?) { + target.onResourceCleared(info.uid, placeholder) + } + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + target.onResourceReady(info.uid, resource) + } + }) } } - override fun loadVideo(holder: VideoViewHolder, info: AttachmentInfo.Video) { + override fun loadVideo(target: VideoLoaderTarget, info: AttachmentInfo.Video) { val data = info.data as? VideoContentRenderer.Data ?: return // videoContentRenderer.render(data, // holder.thumbnailImage, // holder.loaderProgressBar, // holder.videoView, // holder.errorTextView) - imageContentRenderer.render(data.thumbnailMediaData, holder.thumbnailImage, object : CustomViewTarget(holder.thumbnailImage) { + imageContentRenderer.render(data.thumbnailMediaData, target.contextView(), object : CustomViewTarget(target.contextView()) { override fun onLoadFailed(errorDrawable: Drawable?) { - holder.thumbnailImage.setImageDrawable(errorDrawable) + target.onThumbnailLoadFailed(info.uid, errorDrawable) } override fun onResourceCleared(placeholder: Drawable?) { + target.onThumbnailResourceCleared(info.uid, placeholder) } override fun onResourceReady(resource: Drawable, transition: Transition?) { - holder.thumbnailImage.setImageDrawable(resource) + target.onThumbnailResourceReady(info.uid, resource) } }) - holder.thumbnailImage.isVisible = false - holder.loaderProgressBar.isVisible = false - holder.videoView.isVisible = false - + target.onVideoFileLoading(info.uid) fileService.downloadFile( downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, id = data.eventId, @@ -180,11 +206,11 @@ class RoomAttachmentProvider( url = data.url, callback = object : MatrixCallback { override fun onSuccess(data: File) { - holder.videoReady(data) + target.onVideoFileReady(info.uid, data) } override fun onFailure(failure: Throwable) { - holder.videoView.isVisible = false + target.onVideoFileLoadFailed(info.uid) } } ) @@ -214,6 +240,10 @@ class RoomAttachmentProvider( overlayView?.videoControlsGroup?.isVisible = item.root.isVideoMessage() return overlayView } + + override fun clear(id: String) { + // TODO("Not yet implemented") + } } class RoomAttachmentProviderFactory @Inject constructor( From 868d9cf55c06ac786d407ec4fce460fb308dfcaa Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 9 Jul 2020 10:11:10 +0200 Subject: [PATCH 16/41] Cleaning (remove audio and file as not supported yet) --- .../AttachmentSourceProvider.kt | 4 +- .../attachmentviewer/AttachmentsAdapter.kt | 38 +++------------- .../riotx/attachmentviewer/BaseViewHolder.kt | 45 +++++++++++++++++++ 3 files changed, 53 insertions(+), 34 deletions(-) create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/BaseViewHolder.kt diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentSourceProvider.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentSourceProvider.kt index ce725afec2..92a4f1d9e4 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentSourceProvider.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentSourceProvider.kt @@ -23,8 +23,8 @@ sealed class AttachmentInfo(open val uid: String) { data class Image(override val uid: String, val url: String, val data: Any?) : AttachmentInfo(uid) data class AnimatedImage(override val uid: String, val url: String, val data: Any?) : AttachmentInfo(uid) data class Video(override val uid: String, val url: String, val data: Any, val thumbnail: Image?) : AttachmentInfo(uid) - data class Audio(override val uid: String, val url: String, val data: Any) : AttachmentInfo(uid) - data class File(override val uid: String, val url: String, val data: Any) : AttachmentInfo(uid) +// data class Audio(override val uid: String, val url: String, val data: Any) : AttachmentInfo(uid) +// data class File(override val uid: String, val url: String, val data: Any) : AttachmentInfo(uid) } interface AttachmentSourceProvider { diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt index 2f453b58a8..90020f2cb0 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt @@ -17,36 +17,10 @@ package im.vector.riotx.attachmentviewer import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -abstract class BaseViewHolder constructor(itemView: View) : - RecyclerView.ViewHolder(itemView) { - - open fun onRecycled() { - boundResourceUid = null - } - - open fun onAttached() {} - open fun onDetached() {} - open fun entersBackground() {} - open fun entersForeground() {} - open fun onSelected(selected: Boolean) {} - - open fun handleCommand(commands: AttachmentCommands) {} - - var boundResourceUid: String? = null - - open fun bind(attachmentInfo: AttachmentInfo) { - boundResourceUid = attachmentInfo.uid - } -} - -class AttachmentViewHolder constructor(itemView: View) : - BaseViewHolder(itemView) - -class AttachmentsAdapter() : RecyclerView.Adapter() { +class AttachmentsAdapter : RecyclerView.Adapter() { var attachmentSourceProvider: AttachmentSourceProvider? = null set(value) { @@ -71,7 +45,7 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { R.layout.item_image_attachment -> ZoomableImageViewHolder(itemView) R.layout.item_animated_image_attachment -> AnimatedImageViewHolder(itemView) R.layout.item_video_attachment -> VideoViewHolder(itemView) - else -> AttachmentViewHolder(itemView) + else -> UnsupportedViewHolder(itemView) } } @@ -81,8 +55,8 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { is AttachmentInfo.Image -> R.layout.item_image_attachment is AttachmentInfo.Video -> R.layout.item_video_attachment is AttachmentInfo.AnimatedImage -> R.layout.item_animated_image_attachment - is AttachmentInfo.Audio -> TODO() - is AttachmentInfo.File -> TODO() +// is AttachmentInfo.Audio -> TODO() +// is AttachmentInfo.File -> TODO() } } @@ -103,8 +77,8 @@ class AttachmentsAdapter() : RecyclerView.Adapter() { is AttachmentInfo.Video -> { attachmentSourceProvider?.loadVideo((holder as VideoViewHolder).target, it) } - else -> { - } +// else -> { +//// } } } } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/BaseViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/BaseViewHolder.kt new file mode 100644 index 0000000000..49b47c11ff --- /dev/null +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/BaseViewHolder.kt @@ -0,0 +1,45 @@ +/* + * 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.riotx.attachmentviewer + +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +abstract class BaseViewHolder constructor(itemView: View) : + RecyclerView.ViewHolder(itemView) { + + open fun onRecycled() { + boundResourceUid = null + } + + open fun onAttached() {} + open fun onDetached() {} + open fun entersBackground() {} + open fun entersForeground() {} + open fun onSelected(selected: Boolean) {} + + open fun handleCommand(commands: AttachmentCommands) {} + + var boundResourceUid: String? = null + + open fun bind(attachmentInfo: AttachmentInfo) { + boundResourceUid = attachmentInfo.uid + } +} + +class UnsupportedViewHolder constructor(itemView: View) : + BaseViewHolder(itemView) From e38cb7c1a6ced6b7933d14bc0803ee26ce0a3b77 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 9 Jul 2020 10:16:38 +0200 Subject: [PATCH 17/41] Unwanted logs --- .../AttachmentViewerActivity.kt | 19 +++++++++---------- .../attachmentviewer/AttachmentsAdapter.kt | 2 +- .../riotx/attachmentviewer/VideoViewHolder.kt | 3 +-- .../ZoomableImageViewHolder.kt | 3 +-- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt index 029064e058..d6cf7c606a 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt @@ -18,7 +18,6 @@ package im.vector.riotx.attachmentviewer import android.graphics.Color import android.os.Bundle -import android.util.Log import android.view.GestureDetector import android.view.MotionEvent import android.view.ScaleGestureDetector @@ -161,26 +160,26 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi return true } - Log.v("ATTACHEMENTS", "================\ndispatchTouchEvent $ev") + // Log.v("ATTACHEMENTS", "================\ndispatchTouchEvent $ev") handleUpDownEvent(ev) - Log.v("ATTACHEMENTS", "scaleDetector is in progress ${scaleDetector.isInProgress}") - Log.v("ATTACHEMENTS", "pointerCount ${ev.pointerCount}") - Log.v("ATTACHEMENTS", "wasScaled $wasScaled") + // Log.v("ATTACHEMENTS", "scaleDetector is in progress ${scaleDetector.isInProgress}") + // Log.v("ATTACHEMENTS", "pointerCount ${ev.pointerCount}") + // Log.v("ATTACHEMENTS", "wasScaled $wasScaled") if (swipeDirection == null && (scaleDetector.isInProgress || ev.pointerCount > 1 || wasScaled)) { wasScaled = true - Log.v("ATTACHEMENTS", "dispatch to pager") +// Log.v("ATTACHEMENTS", "dispatch to pager") return attachmentPager.dispatchTouchEvent(ev) } - Log.v("ATTACHEMENTS", "is current item scaled ${isScaled()}") + // Log.v("ATTACHEMENTS", "is current item scaled ${isScaled()}") return (if (isScaled()) super.dispatchTouchEvent(ev) else handleTouchIfNotScaled(ev)).also { - Log.v("ATTACHEMENTS", "\n================") +// Log.v("ATTACHEMENTS", "\n================") } } private fun handleUpDownEvent(event: MotionEvent) { - Log.v("ATTACHEMENTS", "handleUpDownEvent $event") + // Log.v("ATTACHEMENTS", "handleUpDownEvent $event") if (event.action == MotionEvent.ACTION_UP) { handleEventActionUp(event) } @@ -232,7 +231,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi } private fun handleTouchIfNotScaled(event: MotionEvent): Boolean { - Log.v("ATTACHEMENTS", "handleTouchIfNotScaled $event") +// Log.v("ATTACHEMENTS", "handleTouchIfNotScaled $event") directionDetector.handleTouchEvent(event) return when (swipeDirection) { diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt index 90020f2cb0..27bdfdc91d 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentsAdapter.kt @@ -78,7 +78,7 @@ class AttachmentsAdapter : RecyclerView.Adapter() { attachmentSourceProvider?.loadVideo((holder as VideoViewHolder).target, it) } // else -> { -//// } +// // } } } } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt index 2b417baecc..e1a5a9864f 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/VideoViewHolder.kt @@ -16,7 +16,6 @@ package im.vector.riotx.attachmentviewer -import android.util.Log import android.view.View import android.widget.ImageView import android.widget.ProgressBar @@ -115,7 +114,7 @@ class VideoViewHolder constructor(itemView: View) : val duration = videoView.duration val progress = videoView.currentPosition val isPlaying = videoView.isPlaying - Log.v("FOO", "isPlaying $isPlaying $progress/$duration") +// Log.v("FOO", "isPlaying $isPlaying $progress/$duration") eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration)) } } diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ZoomableImageViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ZoomableImageViewHolder.kt index aeaf612bbc..3eb06e4c27 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ZoomableImageViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/ZoomableImageViewHolder.kt @@ -16,7 +16,6 @@ package im.vector.riotx.attachmentviewer -import android.util.Log import android.view.View import android.widget.ProgressBar import com.github.chrisbanes.photoview.PhotoView @@ -30,7 +29,7 @@ class ZoomableImageViewHolder constructor(itemView: View) : init { touchImageView.setAllowParentInterceptOnEdge(false) touchImageView.setOnScaleChangeListener { scaleFactor, _, _ -> - Log.v("ATTACHEMENTS", "scaleFactor $scaleFactor") + // Log.v("ATTACHEMENTS", "scaleFactor $scaleFactor") // It's a bit annoying but when you pitch down the scaling // is not exactly one :/ touchImageView.setAllowParentInterceptOnEdge(scaleFactor <= 1.0008f) From 195e2703b91fe9c61b95de99516f0b98c51b1226 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 9 Jul 2020 15:22:34 +0200 Subject: [PATCH 18/41] Support open from upload media tab --- .../features/media/BaseAttachmentProvider.kt | 148 ++++++++++++++++ .../media/DataAttachmentRoomProvider.kt | 112 +++++++++++++ ...der.kt => RoomEventsAttachmentProvider.kt} | 158 ++++-------------- .../media/VectorAttachmentViewerActivity.kt | 96 +++++------ .../features/navigation/DefaultNavigator.kt | 33 ++-- .../riotx/features/navigation/Navigator.kt | 6 +- .../uploads/media/RoomUploadsMediaFragment.kt | 83 ++++++++- .../uploads/media/UploadsImageItem.kt | 2 + .../uploads/media/UploadsVideoItem.kt | 2 + .../main/res/layout/fragment_room_uploads.xml | 2 + 10 files changed, 455 insertions(+), 187 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/media/BaseAttachmentProvider.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/media/DataAttachmentRoomProvider.kt rename vector/src/main/java/im/vector/riotx/features/media/{RoomAttachmentProvider.kt => RoomEventsAttachmentProvider.kt} (53%) diff --git a/vector/src/main/java/im/vector/riotx/features/media/BaseAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/BaseAttachmentProvider.kt new file mode 100644 index 0000000000..d4c41c7cb3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/media/BaseAttachmentProvider.kt @@ -0,0 +1,148 @@ +/* + * 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.riotx.features.media + +import android.content.Context +import android.graphics.drawable.Drawable +import android.view.View +import android.widget.ImageView +import com.bumptech.glide.request.target.CustomViewTarget +import com.bumptech.glide.request.transition.Transition +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.file.FileService +import im.vector.riotx.attachmentviewer.AttachmentInfo +import im.vector.riotx.attachmentviewer.AttachmentSourceProvider +import im.vector.riotx.attachmentviewer.ImageLoaderTarget +import im.vector.riotx.attachmentviewer.VideoLoaderTarget +import java.io.File + +abstract class BaseAttachmentProvider(val imageContentRenderer: ImageContentRenderer, val fileService: FileService) : AttachmentSourceProvider { + + interface InteractionListener { + fun onDismissTapped() + fun onShareTapped() + fun onPlayPause(play: Boolean) + fun videoSeekTo(percent: Int) + } + + var interactionListener: InteractionListener? = null + + protected var overlayView: AttachmentOverlayView? = null + + override fun overlayViewAtPosition(context: Context, position: Int): View? { + if (position == -1) return null + if (overlayView == null) { + overlayView = AttachmentOverlayView(context) + overlayView?.onBack = { + interactionListener?.onDismissTapped() + } + overlayView?.onShareCallback = { + interactionListener?.onShareTapped() + } + overlayView?.onPlayPause = { play -> + interactionListener?.onPlayPause(play) + } + overlayView?.videoSeekTo = { percent -> + interactionListener?.videoSeekTo(percent) + } + } + return overlayView + } + + override fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.Image) { + (info.data as? ImageContentRenderer.Data)?.let { + imageContentRenderer.render(it, target.contextView(), object : CustomViewTarget(target.contextView()) { + override fun onLoadFailed(errorDrawable: Drawable?) { + target.onLoadFailed(info.uid, errorDrawable) + } + + override fun onResourceCleared(placeholder: Drawable?) { + target.onResourceCleared(info.uid, placeholder) + } + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + target.onResourceReady(info.uid, resource) + } + }) + } + } + + override fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.AnimatedImage) { + (info.data as? ImageContentRenderer.Data)?.let { + imageContentRenderer.render(it, target.contextView(), object : CustomViewTarget(target.contextView()) { + override fun onLoadFailed(errorDrawable: Drawable?) { + target.onLoadFailed(info.uid, errorDrawable) + } + + override fun onResourceCleared(placeholder: Drawable?) { + target.onResourceCleared(info.uid, placeholder) + } + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + target.onResourceReady(info.uid, resource) + } + }) + } + } + + override fun loadVideo(target: VideoLoaderTarget, info: AttachmentInfo.Video) { + val data = info.data as? VideoContentRenderer.Data ?: return +// videoContentRenderer.render(data, +// holder.thumbnailImage, +// holder.loaderProgressBar, +// holder.videoView, +// holder.errorTextView) + imageContentRenderer.render(data.thumbnailMediaData, target.contextView(), object : CustomViewTarget(target.contextView()) { + override fun onLoadFailed(errorDrawable: Drawable?) { + target.onThumbnailLoadFailed(info.uid, errorDrawable) + } + + override fun onResourceCleared(placeholder: Drawable?) { + target.onThumbnailResourceCleared(info.uid, placeholder) + } + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + target.onThumbnailResourceReady(info.uid, resource) + } + }) + + target.onVideoFileLoading(info.uid) + fileService.downloadFile( + downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, + id = data.eventId, + mimeType = data.mimeType, + elementToDecrypt = data.elementToDecrypt, + fileName = data.filename, + url = data.url, + callback = object : MatrixCallback { + override fun onSuccess(data: File) { + target.onVideoFileReady(info.uid, data) + } + + override fun onFailure(failure: Throwable) { + target.onVideoFileLoadFailed(info.uid) + } + } + ) + } + + override fun clear(id: String) { + // TODO("Not yet implemented") + } + + abstract fun getFileForSharing(position: Int, callback: ((File?) -> Unit)) +} diff --git a/vector/src/main/java/im/vector/riotx/features/media/DataAttachmentRoomProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/DataAttachmentRoomProvider.kt new file mode 100644 index 0000000000..cb0039fc7e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/media/DataAttachmentRoomProvider.kt @@ -0,0 +1,112 @@ +/* + * 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.riotx.features.media + +import android.content.Context +import android.view.View +import androidx.core.view.isVisible +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.events.model.isVideoMessage +import im.vector.matrix.android.api.session.file.FileService +import im.vector.matrix.android.api.session.room.Room +import im.vector.riotx.attachmentviewer.AttachmentInfo +import im.vector.riotx.core.date.VectorDateFormatter +import im.vector.riotx.core.extensions.localDateTime +import java.io.File + +class DataAttachmentRoomProvider( + private val attachments: List, + private val room: Room?, + private val initialIndex: Int, + imageContentRenderer: ImageContentRenderer, + private val dateFormatter: VectorDateFormatter, + fileService: FileService) : BaseAttachmentProvider(imageContentRenderer, fileService) { + + override fun getItemCount(): Int = attachments.size + + override fun getAttachmentInfoAt(position: Int): AttachmentInfo { + return attachments[position].let { + when (it) { + is ImageContentRenderer.Data -> { + if (it.mimeType == "image/gif") { + AttachmentInfo.AnimatedImage( + uid = it.eventId, + url = it.url ?: "", + data = it + ) + } else { + AttachmentInfo.Image( + uid = it.eventId, + url = it.url ?: "", + data = it + ) + } + } + is VideoContentRenderer.Data -> { + AttachmentInfo.Video( + uid = it.eventId, + url = it.url ?: "", + data = it, + thumbnail = AttachmentInfo.Image( + uid = it.eventId, + url = it.thumbnailMediaData.url ?: "", + data = it.thumbnailMediaData + ) + ) + } + else -> throw IllegalArgumentException() + } + } + } + + override fun overlayViewAtPosition(context: Context, position: Int): View? { + super.overlayViewAtPosition(context, position) + val item = attachments[position] + val timeLineEvent = room?.getTimeLineEvent(item.eventId) + if (timeLineEvent != null) { + val dateString = timeLineEvent.root.localDateTime().let { + "${dateFormatter.formatMessageDay(it)} at ${dateFormatter.formatMessageHour(it)} " + } + overlayView?.updateWith("${position + 1} of ${attachments.size}", "${timeLineEvent.senderInfo.displayName} $dateString") + overlayView?.videoControlsGroup?.isVisible = timeLineEvent.root.isVideoMessage() + } else { + overlayView?.updateWith("", "") + } + return overlayView + } + + override fun getFileForSharing(position: Int, callback: (File?) -> Unit) { + val item = attachments[position] + fileService.downloadFile( + downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, + id = item.eventId, + fileName = item.filename, + mimeType = item.mimeType, + url = item.url ?: "", + elementToDecrypt = item.elementToDecrypt, + callback = object : MatrixCallback { + override fun onSuccess(data: File) { + callback(data) + } + + override fun onFailure(failure: Throwable) { + callback(null) + } + } + ) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/RoomEventsAttachmentProvider.kt similarity index 53% rename from vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt rename to vector/src/main/java/im/vector/riotx/features/media/RoomEventsAttachmentProvider.kt index f7299bf714..7a7fea6dc4 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/RoomEventsAttachmentProvider.kt @@ -17,17 +17,14 @@ package im.vector.riotx.features.media import android.content.Context -import android.graphics.drawable.Drawable import android.view.View -import android.widget.ImageView import androidx.core.view.isVisible -import com.bumptech.glide.request.target.CustomViewTarget -import com.bumptech.glide.request.transition.Transition import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.isVideoMessage import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.file.FileService +import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageImageContent import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent @@ -36,33 +33,18 @@ import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.riotx.attachmentviewer.AttachmentInfo -import im.vector.riotx.attachmentviewer.AttachmentSourceProvider -import im.vector.riotx.attachmentviewer.ImageLoaderTarget -import im.vector.riotx.attachmentviewer.VideoLoaderTarget import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.extensions.localDateTime import java.io.File import javax.inject.Inject -class RoomAttachmentProvider( +class RoomEventsAttachmentProvider( private val attachments: List, private val initialIndex: Int, - private val imageContentRenderer: ImageContentRenderer, - private val videoContentRenderer: VideoContentRenderer, + imageContentRenderer: ImageContentRenderer, private val dateFormatter: VectorDateFormatter, - private val fileService: FileService -) : AttachmentSourceProvider { - - interface InteractionListener { - fun onDismissTapped() - fun onShareTapped() - fun onPlayPause(play: Boolean) - fun videoSeekTo(percent: Int) - } - - var interactionListener: InteractionListener? = null - - private var overlayView: AttachmentOverlayView? = null + fileService: FileService +) : BaseAttachmentProvider(imageContentRenderer, fileService) { override fun getItemCount(): Int { return attachments.size @@ -139,99 +121,8 @@ class RoomAttachmentProvider( } } - override fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.Image) { - (info.data as? ImageContentRenderer.Data)?.let { - imageContentRenderer.render(it, target.contextView(), object : CustomViewTarget(target.contextView()) { - override fun onLoadFailed(errorDrawable: Drawable?) { - target.onLoadFailed(info.uid, errorDrawable) - } - - override fun onResourceCleared(placeholder: Drawable?) { - target.onResourceCleared(info.uid, placeholder) - } - - override fun onResourceReady(resource: Drawable, transition: Transition?) { - target.onResourceReady(info.uid, resource) - } - }) - } - } - - override fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.AnimatedImage) { - (info.data as? ImageContentRenderer.Data)?.let { - imageContentRenderer.render(it, target.contextView(), object : CustomViewTarget(target.contextView()) { - override fun onLoadFailed(errorDrawable: Drawable?) { - target.onLoadFailed(info.uid, errorDrawable) - } - - override fun onResourceCleared(placeholder: Drawable?) { - target.onResourceCleared(info.uid, placeholder) - } - - override fun onResourceReady(resource: Drawable, transition: Transition?) { - target.onResourceReady(info.uid, resource) - } - }) - } - } - - override fun loadVideo(target: VideoLoaderTarget, info: AttachmentInfo.Video) { - val data = info.data as? VideoContentRenderer.Data ?: return -// videoContentRenderer.render(data, -// holder.thumbnailImage, -// holder.loaderProgressBar, -// holder.videoView, -// holder.errorTextView) - imageContentRenderer.render(data.thumbnailMediaData, target.contextView(), object : CustomViewTarget(target.contextView()) { - override fun onLoadFailed(errorDrawable: Drawable?) { - target.onThumbnailLoadFailed(info.uid, errorDrawable) - } - - override fun onResourceCleared(placeholder: Drawable?) { - target.onThumbnailResourceCleared(info.uid, placeholder) - } - - override fun onResourceReady(resource: Drawable, transition: Transition?) { - target.onThumbnailResourceReady(info.uid, resource) - } - }) - - target.onVideoFileLoading(info.uid) - fileService.downloadFile( - downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, - id = data.eventId, - mimeType = data.mimeType, - elementToDecrypt = data.elementToDecrypt, - fileName = data.filename, - url = data.url, - callback = object : MatrixCallback { - override fun onSuccess(data: File) { - target.onVideoFileReady(info.uid, data) - } - - override fun onFailure(failure: Throwable) { - target.onVideoFileLoadFailed(info.uid) - } - } - ) - } - override fun overlayViewAtPosition(context: Context, position: Int): View? { - if (overlayView == null) { - overlayView = AttachmentOverlayView(context) - overlayView?.onBack = { - interactionListener?.onDismissTapped() - } - overlayView?.onShareCallback = { - interactionListener?.onShareTapped() - } - overlayView?.onPlayPause = { play -> - interactionListener?.onPlayPause(play) - } - overlayView?.videoSeekTo = { percent -> - interactionListener?.videoSeekTo(percent) - } - } + super.overlayViewAtPosition(context, position) val item = attachments[position] val dateString = item.root.localDateTime().let { "${dateFormatter.formatMessageDay(it)} at ${dateFormatter.formatMessageHour(it)} " @@ -241,19 +132,44 @@ class RoomAttachmentProvider( return overlayView } - override fun clear(id: String) { - // TODO("Not yet implemented") + override fun getFileForSharing(position: Int, callback: (File?) -> Unit) { + attachments[position].let { timelineEvent -> + + val messageContent = timelineEvent.root.getClearContent().toModel() + as? MessageWithAttachmentContent + ?: return@let + fileService.downloadFile( + downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, + id = timelineEvent.eventId, + fileName = messageContent.body, + mimeType = messageContent.mimeType, + url = messageContent.getFileUrl(), + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), + callback = object : MatrixCallback { + override fun onSuccess(data: File) { + callback(data) + } + + override fun onFailure(failure: Throwable) { + callback(null) + } + } + ) + } } } -class RoomAttachmentProviderFactory @Inject constructor( +class AttachmentProviderFactory @Inject constructor( private val imageContentRenderer: ImageContentRenderer, private val vectorDateFormatter: VectorDateFormatter, - private val videoContentRenderer: VideoContentRenderer, private val session: Session ) { - fun createProvider(attachments: List, initialIndex: Int): RoomAttachmentProvider { - return RoomAttachmentProvider(attachments, initialIndex, imageContentRenderer, videoContentRenderer, vectorDateFormatter, session.fileService()) + fun createProvider(attachments: List, initialIndex: Int): RoomEventsAttachmentProvider { + return RoomEventsAttachmentProvider(attachments, initialIndex, imageContentRenderer, vectorDateFormatter, session.fileService()) + } + + fun createProvider(attachments: List, room: Room?, initialIndex: Int): DataAttachmentRoomProvider { + return DataAttachmentRoomProvider(attachments, room, initialIndex, imageContentRenderer, vectorDateFormatter, session.fileService()) } } diff --git a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt index 10483f3fa9..c0b822c13a 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt @@ -30,14 +30,6 @@ import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.transition.Transition -import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.file.FileService -import im.vector.matrix.android.api.session.room.model.message.MessageContent -import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent -import im.vector.matrix.android.api.session.room.model.message.getFileUrl -import im.vector.matrix.android.api.session.room.timeline.TimelineEvent -import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.riotx.R import im.vector.riotx.attachmentviewer.AttachmentCommands import im.vector.riotx.attachmentviewer.AttachmentViewerActivity @@ -52,11 +44,10 @@ import im.vector.riotx.features.themes.ActivityOtherThemes import im.vector.riotx.features.themes.ThemeUtils import kotlinx.android.parcel.Parcelize import timber.log.Timber -import java.io.File import javax.inject.Inject import kotlin.system.measureTimeMillis -class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmentProvider.InteractionListener { +class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmentProvider.InteractionListener { @Parcelize data class Args( @@ -69,7 +60,7 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen lateinit var sessionHolder: ActiveSessionHolder @Inject - lateinit var dataSourceFactory: RoomAttachmentProviderFactory + lateinit var dataSourceFactory: AttachmentProviderFactory @Inject lateinit var imageContentRenderer: ImageContentRenderer @@ -78,7 +69,8 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen private var initialIndex = 0 private var isAnimatingOut = false - private var eventList: List? = null + + var currentSourceProvider: BaseAttachmentProvider? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -92,13 +84,6 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen ThemeUtils.setActivityTheme(this, getOtherThemes()) val args = args() ?: throw IllegalArgumentException("Missing arguments") - val session = sessionHolder.getSafeActiveSession() ?: return Unit.also { finish() } - - val room = args.roomId?.let { session.getRoom(it) } - val events = room?.getAttachmentMessages() ?: emptyList() - eventList = events - val index = events.indexOfFirst { it.eventId == args.eventId } - initialIndex = index if (savedInstanceState == null && addTransitionListener()) { args.sharedTransitionName?.let { @@ -127,14 +112,41 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen } } - val sourceProvider = dataSourceFactory.createProvider(events, index) - sourceProvider.interactionListener = this - setSourceProvider(sourceProvider) - if (savedInstanceState == null) { - pager2.setCurrentItem(index, false) - // The page change listener is not notified of the change... - pager2.post { - onSelectedPositionChanged(index) + val session = sessionHolder.getSafeActiveSession() ?: return Unit.also { finish() } + + val room = args.roomId?.let { session.getRoom(it) } + + val inMemoryData = intent.getParcelableArrayListExtra(EXTRA_IN_MEMORY_DATA) + if (inMemoryData != null) { + val sourceProvider = dataSourceFactory.createProvider(inMemoryData, room, initialIndex) + val index = inMemoryData.indexOfFirst { it.eventId == args.eventId } + initialIndex = index + sourceProvider.interactionListener = this + setSourceProvider(sourceProvider) + this.currentSourceProvider = sourceProvider + if (savedInstanceState == null) { + pager2.setCurrentItem(index, false) + // The page change listener is not notified of the change... + pager2.post { + onSelectedPositionChanged(index) + } + } + } else { + val events = room?.getAttachmentMessages() + ?: emptyList() + val index = events.indexOfFirst { it.eventId == args.eventId } + initialIndex = index + + val sourceProvider = dataSourceFactory.createProvider(events, index) + sourceProvider.interactionListener = this + setSourceProvider(sourceProvider) + this.currentSourceProvider = sourceProvider + if (savedInstanceState == null) { + pager2.setCurrentItem(index, false) + // The page change listener is not notified of the change... + pager2.post { + onSelectedPositionChanged(index) + } } } @@ -228,14 +240,19 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen const val EXTRA_ARGS = "EXTRA_ARGS" const val EXTRA_IMAGE_DATA = "EXTRA_IMAGE_DATA" + const val EXTRA_IN_MEMORY_DATA = "EXTRA_IN_MEMORY_DATA" fun newIntent(context: Context, mediaData: AttachmentData, roomId: String?, eventId: String, + inMemoryData: List?, sharedTransitionName: String?) = Intent(context, VectorAttachmentViewerActivity::class.java).also { it.putExtra(EXTRA_ARGS, Args(roomId, eventId, sharedTransitionName)) it.putExtra(EXTRA_IMAGE_DATA, mediaData) + if (inMemoryData != null) { + it.putParcelableArrayListExtra(EXTRA_IN_MEMORY_DATA, ArrayList(inMemoryData)) + } } } @@ -252,27 +269,10 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen } override fun onShareTapped() { - // Share - eventList?.get(currentPosition)?.let { timelineEvent -> - - val messageContent = timelineEvent.root.getClearContent().toModel() - as? MessageWithAttachmentContent - ?: return@let - sessionHolder.getSafeActiveSession()?.fileService()?.downloadFile( - downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE, - id = timelineEvent.eventId, - fileName = messageContent.body, - mimeType = messageContent.mimeType, - url = messageContent.getFileUrl(), - elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), - callback = object : MatrixCallback { - override fun onSuccess(data: File) { - if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { - shareMedia(this@VectorAttachmentViewerActivity, data, getMimeTypeFromUri(this@VectorAttachmentViewerActivity, data.toUri())) - } - } - } - ) + this.currentSourceProvider?.getFileForSharing(currentPosition) { data -> + if (data != null && lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { + shareMedia(this@VectorAttachmentViewerActivity, data, getMimeTypeFromUri(this@VectorAttachmentViewerActivity, data.toUri())) + } } } } diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 2b0b6175f5..8940ac6791 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -246,20 +246,25 @@ class DefaultNavigator @Inject constructor( } override fun openImageViewer(activity: Activity, - roomId: String?, + roomId: String, mediaData: AttachmentData, view: View, + inMemory: List?, options: ((MutableList>) -> Unit)?) { - VectorAttachmentViewerActivity.newIntent(activity, mediaData, roomId, mediaData.eventId, ViewCompat.getTransitionName(view)).let { intent -> + VectorAttachmentViewerActivity.newIntent(activity, + mediaData, + roomId, + mediaData.eventId, + inMemory, + ViewCompat.getTransitionName(view)).let { intent -> val pairs = ArrayList>() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let { - pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) - } - activity.window.decorView.findViewById(android.R.id.navigationBarBackground)?.let { - pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) - } + activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let { + pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) } + activity.window.decorView.findViewById(android.R.id.navigationBarBackground)?.let { + pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) + } + pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: "")) options?.invoke(pairs) @@ -284,12 +289,18 @@ class DefaultNavigator @Inject constructor( } override fun openVideoViewer(activity: Activity, - roomId: String?, mediaData: VideoContentRenderer.Data, + roomId: String, mediaData: VideoContentRenderer.Data, view: View, + inMemory: List?, options: ((MutableList>) -> Unit)?) { // val intent = VideoMediaViewerActivity.newIntent(activity, mediaData) // activity.startActivity(intent) - VectorAttachmentViewerActivity.newIntent(activity, mediaData, roomId, mediaData.eventId, ViewCompat.getTransitionName(view)).let { intent -> + VectorAttachmentViewerActivity.newIntent(activity, + mediaData, + roomId, + mediaData.eventId, + inMemory, + ViewCompat.getTransitionName(view)).let { intent -> val pairs = ArrayList>() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let { diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index f1be6e072b..f925344570 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -92,13 +92,15 @@ interface Navigator { fun openRoomWidget(context: Context, roomId: String, widget: Widget) fun openImageViewer(activity: Activity, - roomId: String?, + roomId: String, mediaData: AttachmentData, view: View, + inMemory: List? = null, options: ((MutableList>) -> Unit)?) fun openVideoViewer(activity: Activity, - roomId: String?, mediaData: VideoContentRenderer.Data, + roomId: String, mediaData: VideoContentRenderer.Data, view: View, + inMemory: List? = null, options: ((MutableList>) -> Unit)?) } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt index a5f126875a..e0758c7d72 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt @@ -20,23 +20,34 @@ import android.os.Bundle import android.util.DisplayMetrics import android.view.View import androidx.core.content.ContextCompat +import androidx.core.util.Pair +import androidx.core.view.ViewCompat import androidx.recyclerview.widget.GridLayoutManager import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState +import com.google.android.material.appbar.AppBarLayout +import im.vector.matrix.android.api.session.room.model.message.MessageImageContent +import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent +import im.vector.matrix.android.api.session.room.model.message.getFileUrl +import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.riotx.R import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.trackItemsVisibilityChange import im.vector.riotx.core.platform.StateView import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.utils.DimensionConverter +import im.vector.riotx.features.media.AttachmentData import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VideoContentRenderer import im.vector.riotx.features.roomprofile.uploads.RoomUploadsAction +import im.vector.riotx.features.roomprofile.uploads.RoomUploadsFragment import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewModel +import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewState import kotlinx.android.synthetic.main.fragment_generic_state_view_recycler.* +import kotlinx.android.synthetic.main.fragment_room_uploads.* import javax.inject.Inject class RoomUploadsMediaFragment @Inject constructor( @@ -76,13 +87,75 @@ class RoomUploadsMediaFragment @Inject constructor( controller.listener = null } - override fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) { - navigator.openImageViewer(requireActivity(), null, mediaData, view, null) + // It's very strange i can't just access + // the app bar using find by id... + private fun trickFindAppBar() : AppBarLayout? { + return activity?.supportFragmentManager?.fragments + ?.filterIsInstance() + ?.firstOrNull() + ?.roomUploadsAppBar } - override fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) { - // TODO - // navigator.openVideoViewer(requireActivity(), mediaData, null, ) + override fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) = withState(uploadsViewModel) { state -> + + val inMemory = getItemsArgs(state) + navigator.openImageViewer(requireActivity(), state.roomId, mediaData, view, inMemory) { pairs -> + trickFindAppBar()?.let { + pairs.add(Pair(it, ViewCompat.getTransitionName(it) ?: "")) + } + } + } + + private fun getItemsArgs(state: RoomUploadsViewState): List { + return state.mediaEvents.mapNotNull { + when (val content = it.contentWithAttachmentContent) { + is MessageImageContent -> { + ImageContentRenderer.Data( + eventId = it.eventId, + filename = content.body, + mimeType = content.mimeType, + url = content.getFileUrl(), + elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(), + maxHeight = -1, + maxWidth = -1, + width = null, + height = null + ) + } + is MessageVideoContent -> { + val thumbnailData = ImageContentRenderer.Data( + eventId = it.eventId, + filename = content.body, + mimeType = content.mimeType, + url = content.videoInfo?.thumbnailFile?.url + ?: content.videoInfo?.thumbnailUrl, + elementToDecrypt = content.videoInfo?.thumbnailFile?.toElementToDecrypt(), + height = content.videoInfo?.height, + maxHeight = -1, + width = content.videoInfo?.width, + maxWidth = -1 + ) + VideoContentRenderer.Data( + eventId = it.eventId, + filename = content.body, + mimeType = content.mimeType, + url = content.getFileUrl(), + elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(), + thumbnailMediaData = thumbnailData + ) + } + else -> null + } + } + } + + override fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) = withState(uploadsViewModel) { state -> + val inMemory = getItemsArgs(state) + navigator.openVideoViewer(requireActivity(), state.roomId, mediaData, view, inMemory) { pairs -> + trickFindAppBar()?.let { + pairs.add(Pair(it, ViewCompat.getTransitionName(it) ?: "")) + } + } } override fun loadMore() { diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt index 98026901cc..f994ad0110 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.roomprofile.uploads.media import android.view.View import android.widget.ImageView +import androidx.core.view.ViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R @@ -37,6 +38,7 @@ abstract class UploadsImageItem : VectorEpoxyModel() { super.bind(holder) holder.view.setOnClickListener { listener?.onItemClicked(holder.imageView, data) } imageContentRenderer.render(data, holder.imageView, IMAGE_SIZE_DP) + ViewCompat.setTransitionName(holder.imageView, "imagePreview_${id()}") } class Holder : VectorEpoxyHolder() { diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt index 82e33b76da..1c9ab4ae74 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.roomprofile.uploads.media import android.view.View import android.widget.ImageView +import androidx.core.view.ViewCompat import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R @@ -38,6 +39,7 @@ abstract class UploadsVideoItem : VectorEpoxyModel() { super.bind(holder) holder.view.setOnClickListener { listener?.onItemClicked(holder.imageView, data) } imageContentRenderer.render(data.thumbnailMediaData, holder.imageView, IMAGE_SIZE_DP) + ViewCompat.setTransitionName(holder.imageView, "videoPreview_${id()}") } class Holder : VectorEpoxyHolder() { diff --git a/vector/src/main/res/layout/fragment_room_uploads.xml b/vector/src/main/res/layout/fragment_room_uploads.xml index 5e289d4724..f5d3658ee5 100644 --- a/vector/src/main/res/layout/fragment_room_uploads.xml +++ b/vector/src/main/res/layout/fragment_room_uploads.xml @@ -8,6 +8,8 @@ From a98b2ecce39147f50a8ac5b150401ce5a4a7f7f6 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 30 Jun 2020 15:59:43 +0200 Subject: [PATCH 19/41] Set server backup banner --- .../vector/riotx/core/di/ScreenComponent.kt | 2 + .../vector/riotx/core/di/ViewModelModule.kt | 7 +- .../riotx/core/ui/views/KeysBackupBanner.kt | 49 ++--- .../recover/BootstrapCrossSigningTask.kt | 30 ++- .../riotx/features/home/HomeActivity.kt | 15 +- .../riotx/features/home/HomeDetailFragment.kt | 52 ++---- .../features/navigation/DefaultNavigator.kt | 10 +- .../signout/ServerBackupStatusViewModel.kt | 176 ++++++++++++++++++ .../SignOutBottomSheetDialogFragment.kt | 24 ++- .../workers/signout/SignOutViewModel.kt | 74 -------- .../main/res/drawable/ic_secure_backup.xml | 20 ++ .../main/res/layout/fragment_home_detail.xml | 2 + .../res/layout/view_keys_backup_banner.xml | 12 +- vector/src/main/res/values/strings.xml | 7 +- 14 files changed, 312 insertions(+), 168 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt delete mode 100644 vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutViewModel.kt create mode 100644 vector/src/main/res/drawable/ic_secure_backup.xml diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index ceb276614a..4a6aee0f6f 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -72,6 +72,7 @@ import im.vector.riotx.features.terms.ReviewTermsActivity import im.vector.riotx.features.ui.UiStateRepository import im.vector.riotx.features.widgets.WidgetActivity import im.vector.riotx.features.widgets.permissions.RoomWidgetPermissionBottomSheet +import im.vector.riotx.features.workers.signout.SignOutBottomSheetDialogFragment @Component( dependencies = [ @@ -152,6 +153,7 @@ interface ScreenComponent { fun inject(bottomSheet: RoomWidgetPermissionBottomSheet) fun inject(bottomSheet: RoomWidgetsBottomSheet) fun inject(bottomSheet: CallControlsBottomSheet) + fun inject(bottomSheet: SignOutBottomSheetDialogFragment) /* ========================================================================================== * Others diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index badfdd96c1..2a3db0cf19 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -36,7 +36,7 @@ import im.vector.riotx.features.reactions.EmojiChooserViewModel import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.riotx.features.roomprofile.RoomProfileSharedActionViewModel import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel -import im.vector.riotx.features.workers.signout.SignOutViewModel +import im.vector.riotx.features.workers.signout.ServerBackupStatusViewModel @Module interface ViewModelModule { @@ -51,11 +51,6 @@ interface ViewModelModule { * Below are bindings for the androidx view models (which extend ViewModel). Will be converted to MvRx ViewModel in the future. */ - @Binds - @IntoMap - @ViewModelKey(SignOutViewModel::class) - fun bindSignOutViewModel(viewModel: SignOutViewModel): ViewModel - @Binds @IntoMap @ViewModelKey(EmojiChooserViewModel::class) diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt index 817575d91a..460c871288 100755 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt @@ -24,8 +24,10 @@ import android.view.ViewGroup import android.widget.AbsListView import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout +import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.edit import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView import androidx.transition.TransitionManager import butterknife.BindView import butterknife.ButterKnife @@ -58,22 +60,12 @@ class KeysBackupBanner @JvmOverloads constructor( var delegate: Delegate? = null private var state: State = State.Initial - private var scrollState = AbsListView.OnScrollListener.SCROLL_STATE_IDLE - set(value) { - field = value - - val pendingV = pendingVisibility - - if (pendingV != null) { - pendingVisibility = null - visibility = pendingV - } - } - - private var pendingVisibility: Int? = null - init { setupView() + PreferenceManager.getDefaultSharedPreferences(context).edit { + putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false) + putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, "") + } } /** @@ -91,7 +83,8 @@ class KeysBackupBanner @JvmOverloads constructor( state = newState hideAll() - + val parent = parent as ViewGroup + TransitionManager.beginDelayedTransition(parent) when (newState) { State.Initial -> renderInitial() State.Hidden -> renderHidden() @@ -102,22 +95,6 @@ class KeysBackupBanner @JvmOverloads constructor( } } - override fun setVisibility(visibility: Int) { - if (scrollState != AbsListView.OnScrollListener.SCROLL_STATE_IDLE) { - // Wait for scroll state to be idle - pendingVisibility = visibility - return - } - - if (visibility != getVisibility()) { - // Schedule animation - val parent = parent as ViewGroup - TransitionManager.beginDelayedTransition(parent) - } - - super.setVisibility(visibility) - } - override fun onClick(v: View?) { when (state) { is State.Setup -> { @@ -166,6 +143,8 @@ class KeysBackupBanner @JvmOverloads constructor( ButterKnife.bind(this) setOnClickListener(this) + textView1.setOnClickListener(this) + textView2.setOnClickListener(this) } private fun renderInitial() { @@ -218,10 +197,10 @@ class KeysBackupBanner @JvmOverloads constructor( } private fun renderBackingUp() { - // Do not render when backing up anymore - isVisible = false - - textView1.setText(R.string.keys_backup_banner_in_progress) + isVisible = true + textView1.setText(R.string.keys_backup_banner_setup_line1) + textView2.isVisible = true + textView2.setText(R.string.keys_backup_banner_in_progress) loading.isVisible = true } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt index 6a3fadbcb3..290a08bfad 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.securestorage.SsssKeySpec import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.matrix.android.internal.util.awaitCallback @@ -84,8 +85,10 @@ class BootstrapCrossSigningTask @Inject constructor( override suspend fun execute(params: Params): BootstrapResult { val crossSigningService = session.cryptoService().crossSigningService() + Timber.d("## BootstrapCrossSigningTask: initXSOnly:${params.initOnlyCrossSigning} Starting...") // Ensure cross-signing is initialized. Due to migration it is maybe not always correctly initialized if (!crossSigningService.isCrossSigningInitialized()) { + Timber.d("## BootstrapCrossSigningTask: Cross signing not enabled, so initialize") params.progressListener?.onProgress( WaitingViewData( stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing), @@ -104,8 +107,9 @@ class BootstrapCrossSigningTask @Inject constructor( return handleInitializeXSigningError(failure) } } else { - // not sure how this can happen?? + Timber.d("## BootstrapCrossSigningTask: Cross signing already setup, go to 4S setup") if (params.initOnlyCrossSigning) { + // not sure how this can happen?? return handleInitializeXSigningError(IllegalArgumentException("Cross signing already setup")) } } @@ -119,6 +123,8 @@ class BootstrapCrossSigningTask @Inject constructor( stringProvider.getString(R.string.bootstrap_crosssigning_progress_pbkdf2), isIndeterminate = true) ) + + Timber.d("## BootstrapCrossSigningTask: Creating 4S key with pass: ${params.passphrase != null}") try { keyInfo = awaitCallback { params.passphrase?.let { passphrase -> @@ -141,6 +147,7 @@ class BootstrapCrossSigningTask @Inject constructor( } } } catch (failure: Failure) { + Timber.e("## BootstrapCrossSigningTask: Creating 4S - Failed to generate key <${failure.localizedMessage}>") return BootstrapResult.FailedToCreateSSSSKey(failure) } @@ -149,19 +156,25 @@ class BootstrapCrossSigningTask @Inject constructor( stringProvider.getString(R.string.bootstrap_crosssigning_progress_default_key), isIndeterminate = true) ) + + Timber.d("## BootstrapCrossSigningTask: Creating 4S - Set default key") try { awaitCallback { ssssService.setDefaultKey(keyInfo.keyId, it) } } catch (failure: Failure) { // Maybe we could just ignore this error? + Timber.e("## BootstrapCrossSigningTask: Creating 4S - Set default key error <${failure.localizedMessage}>") return BootstrapResult.FailedToSetDefaultSSSSKey(failure) } + + Timber.d("## BootstrapCrossSigningTask: Creating 4S - gathering private keys") val xKeys = crossSigningService.getCrossSigningPrivateKeys() val mskPrivateKey = xKeys?.master ?: return BootstrapResult.MissingPrivateKey val sskPrivateKey = xKeys.selfSigned ?: return BootstrapResult.MissingPrivateKey val uskPrivateKey = xKeys.user ?: return BootstrapResult.MissingPrivateKey + Timber.d("## BootstrapCrossSigningTask: Creating 4S - gathering private keys success") try { params.progressListener?.onProgress( @@ -170,6 +183,7 @@ class BootstrapCrossSigningTask @Inject constructor( isIndeterminate = true ) ) + Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing MSK...") awaitCallback { ssssService.storeSecret( MASTER_KEY_SSSS_NAME, @@ -183,6 +197,7 @@ class BootstrapCrossSigningTask @Inject constructor( isIndeterminate = true ) ) + Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing USK...") awaitCallback { ssssService.storeSecret( USER_SIGNING_KEY_SSSS_NAME, @@ -196,6 +211,7 @@ class BootstrapCrossSigningTask @Inject constructor( stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_ssk), isIndeterminate = true ) ) + Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing SSK...") awaitCallback { ssssService.storeSecret( SELF_SIGNING_KEY_SSSS_NAME, @@ -204,6 +220,7 @@ class BootstrapCrossSigningTask @Inject constructor( ) } } catch (failure: Failure) { + Timber.e("## BootstrapCrossSigningTask: Creating 4S - Failed to store keys <${failure.localizedMessage}>") // Maybe we could just ignore this error? return BootstrapResult.FailedToStorePrivateKeyInSSSS(failure) } @@ -215,7 +232,14 @@ class BootstrapCrossSigningTask @Inject constructor( ) ) try { - if (session.cryptoService().keysBackupService().keysBackupVersion == null) { + Timber.d("## BootstrapCrossSigningTask: Creating 4S - Checking megolm backup") + + // First ensure that in sync + val serverVersion = awaitCallback { + session.cryptoService().keysBackupService().getCurrentVersion(it) + } + if (serverVersion == null) { + Timber.d("## BootstrapCrossSigningTask: Creating 4S - Create megolm backup") val creationInfo = awaitCallback { session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) } @@ -223,6 +247,7 @@ class BootstrapCrossSigningTask @Inject constructor( session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it) } // Save it for gossiping + Timber.d("## BootstrapCrossSigningTask: Creating 4S - Save megolm backup key for gossiping") session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) awaitCallback { @@ -239,6 +264,7 @@ class BootstrapCrossSigningTask @Inject constructor( Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup") } + Timber.d("## BootstrapCrossSigningTask: initXSOnly:${params.initOnlyCrossSigning} Finished") return BootstrapResult.Success(keyInfo) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index 8d5fc5f564..6991aaa493 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -46,7 +46,8 @@ import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.popup.VerificationVectorAlert import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotx.features.settings.VectorPreferences -import im.vector.riotx.features.workers.signout.SignOutViewModel +import im.vector.riotx.features.workers.signout.ServerBackupStatusViewModel +import im.vector.riotx.features.workers.signout.ServerBackupStatusViewState import im.vector.riotx.push.fcm.FcmHelper import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.activity_home.* @@ -60,13 +61,17 @@ data class HomeActivityArgs( val accountCreation: Boolean ) : Parcelable -class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory { +class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory, ServerBackupStatusViewModel.Factory { private lateinit var sharedActionViewModel: HomeSharedActionViewModel private val homeActivityViewModel: HomeActivityViewModel by viewModel() @Inject lateinit var viewModelFactory: HomeActivityViewModel.Factory + + private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel() + @Inject lateinit var serverBackupviewModelFactory: ServerBackupStatusViewModel.Factory + @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @Inject lateinit var pushManager: PushersManager @@ -92,6 +97,10 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet return unknownDeviceViewModelFactory.create(initialState) } + override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel { + return serverBackupviewModelFactory.create(initialState) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice()) @@ -230,7 +239,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet } // Force remote backup state update to update the banner if needed - viewModelProvider.get(SignOutViewModel::class.java).refreshRemoteStateIfNeeded() + serverBackupStatusViewModel.refreshRemoteStateIfNeeded() } override fun configure(toolbar: Toolbar) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index c92c28079f..c736c0c1ca 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -21,13 +21,13 @@ import android.view.LayoutInflater import android.view.View import androidx.core.content.ContextCompat import androidx.core.view.forEachIndexed +import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.bottomnavigation.BottomNavigationItemView import com.google.android.material.bottomnavigation.BottomNavigationMenuView -import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo @@ -49,13 +49,10 @@ import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.popup.VerificationVectorAlert import im.vector.riotx.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS -import im.vector.riotx.features.workers.signout.SignOutViewModel +import im.vector.riotx.features.workers.signout.BannerState +import im.vector.riotx.features.workers.signout.ServerBackupStatusViewModel +import im.vector.riotx.features.workers.signout.ServerBackupStatusViewState import kotlinx.android.synthetic.main.fragment_home_detail.* -import kotlinx.android.synthetic.main.fragment_home_detail.activeCallPiP -import kotlinx.android.synthetic.main.fragment_home_detail.activeCallPiPWrap -import kotlinx.android.synthetic.main.fragment_home_detail.activeCallView -import kotlinx.android.synthetic.main.fragment_home_detail.syncStateView -import kotlinx.android.synthetic.main.fragment_room_detail.* import timber.log.Timber import javax.inject.Inject @@ -65,15 +62,17 @@ private const val INDEX_ROOMS = 2 class HomeDetailFragment @Inject constructor( val homeDetailViewModelFactory: HomeDetailViewModel.Factory, + private val serverBackupStatusViewModelFactory: ServerBackupStatusViewModel.Factory, private val avatarRenderer: AvatarRenderer, private val alertManager: PopupAlertManager, private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager -) : VectorBaseFragment(), KeysBackupBanner.Delegate, ActiveCallView.Callback { +) : VectorBaseFragment(), KeysBackupBanner.Delegate, ActiveCallView.Callback, ServerBackupStatusViewModel.Factory { private val unreadCounterBadgeViews = arrayListOf() private val viewModel: HomeDetailViewModel by fragmentViewModel() private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel() + private val serverBackupStatusViewModel: ServerBackupStatusViewModel by activityViewModel() private lateinit var sharedActionViewModel: HomeSharedActionViewModel private lateinit var sharedCallActionViewModel: SharedActiveCallViewModel @@ -195,34 +194,15 @@ class HomeDetailFragment @Inject constructor( } private fun setupKeysBackupBanner() { - // Keys backup banner - // Use the SignOutViewModel, it observe the keys backup state and this is what we need here - val model = fragmentViewModelProvider.get(SignOutViewModel::class.java) - model.keysBackupState.observe(viewLifecycleOwner, Observer { keysBackupState -> - when (keysBackupState) { - null -> - homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false) - KeysBackupState.Disabled -> - homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(model.getNumberOfKeysToBackup()), false) - KeysBackupState.NotTrusted, - KeysBackupState.WrongBackUpVersion -> - // In this case, getCurrentBackupVersion() should not return "" - homeKeysBackupBanner.render(KeysBackupBanner.State.Recover(model.getCurrentBackupVersion()), false) - KeysBackupState.WillBackUp, - KeysBackupState.BackingUp -> - homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false) - KeysBackupState.ReadyToBackUp -> - if (model.canRestoreKeys()) { - homeKeysBackupBanner.render(KeysBackupBanner.State.Update(model.getCurrentBackupVersion()), false) - } else { - homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false) - } - else -> - homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false) + serverBackupStatusViewModel.subscribe(this) { + when (val banState = it.bannerState.invoke()) { + is BannerState.Setup -> homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false) + BannerState.BackingUp -> homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false) + null, + BannerState.Hidden -> homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false) } - }) - + }.disposeOnDestroyView() homeKeysBackupBanner.delegate = this } @@ -331,4 +311,8 @@ class HomeDetailFragment @Inject constructor( } } } + + override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel { + return serverBackupStatusViewModelFactory.create(initialState) + } } diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 0b89ab8ec4..79ba5121fc 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -199,7 +199,15 @@ class DefaultNavigator @Inject constructor( } override fun openKeysBackupSetup(context: Context, showManualExport: Boolean) { - context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport)) + // if cross signing is enabled we should propose full 4S + sessionHolder.getSafeActiveSession()?.let { session -> + if (session.cryptoService().crossSigningService().canCrossSign() && context is VectorBaseActivity) { + BootstrapBottomSheet.show(context.supportFragmentManager, false) + } else { + context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport)) + } + } + } override fun openKeysBackupManager(context: Context) { diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt new file mode 100644 index 0000000000..04ece8e407 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt @@ -0,0 +1,176 @@ +/* + * Copyright 2019 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.riotx.features.workers.signout + +import androidx.lifecycle.MutableLiveData +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo +import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener +import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData +import im.vector.matrix.rx.rx +import im.vector.riotx.core.platform.EmptyAction +import im.vector.riotx.core.platform.EmptyViewEvents +import im.vector.riotx.core.platform.VectorViewModel +import io.reactivex.Observable +import io.reactivex.functions.Function4 +import io.reactivex.subjects.PublishSubject +import java.util.concurrent.TimeUnit + +data class ServerBackupStatusViewState( + val bannerState: Async = Uninitialized +) : MvRxState + +/** + * The state representing the view + * It can take one state at a time + */ +sealed class BannerState { + + object Hidden : BannerState() + + // Keys backup is not setup, numberOfKeys is the number of locally stored keys + data class Setup(val numberOfKeys: Int) : BannerState() + + // Keys are backing up + object BackingUp : BannerState() +} + +class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialState: ServerBackupStatusViewState, + private val session: Session) + : VectorViewModel(initialState), KeysBackupStateListener { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: ServerBackupStatusViewState): ServerBackupStatusViewModel? { + 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") + } + } + + // Keys exported manually + val keysExportedToFile = MutableLiveData() + val keysBackupState = MutableLiveData() + + private val keyBackupPublishSubject: PublishSubject = PublishSubject.create() + + init { + session.cryptoService().keysBackupService().addListener(this) + + keyBackupPublishSubject.onNext(session.cryptoService().keysBackupService().state) + keysBackupState.value = session.cryptoService().keysBackupService().state + session.rx().liveCrossSigningPrivateKeys() + Observable.combineLatest, Optional, KeysBackupState, Optional, BannerState>( + session.rx().liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME)), + session.rx().liveCrossSigningInfo(session.myUserId), + keyBackupPublishSubject, + session.rx().liveCrossSigningPrivateKeys(), + Function4 { _, crossSigningInfo, keyBackupState, pInfo -> + // first check if 4S is already setup + if (session.sharedSecretStorageService.isRecoverySetup()) { + // 4S is already setup sp we should not display anything + return@Function4 when (keyBackupState) { + KeysBackupState.BackingUp -> BannerState.BackingUp + else -> BannerState.Hidden + } + } + + // So recovery is not setup + // Check if cross signing is enabled and local secrets known + if (crossSigningInfo.getOrNull()?.isTrusted() == true + && pInfo.getOrNull()?.master != null + && pInfo.getOrNull()?.selfSigned != null + && pInfo.getOrNull()?.user != null + ) { + // So 4S is not setup and we have local secrets, + return@Function4 BannerState.Setup(numberOfKeys = getNumberOfKeysToBackup()) + } + + BannerState.Hidden + } + ) + .throttleLast(2000, TimeUnit.MILLISECONDS) // we don't want to flicker or catch transient states + .distinctUntilChanged() + .execute { async -> + copy( + bannerState = async + ) + } + } + + /** + * Safe way to get the current KeysBackup version + */ + fun getCurrentBackupVersion(): String { + return session.cryptoService().keysBackupService().currentBackupVersion ?: "" + } + + /** + * Safe way to get the number of keys to backup + */ + fun getNumberOfKeysToBackup(): Int { + return session.cryptoService().inboundGroupSessionsCount(false) + } + + /** + * Safe way to tell if there are more keys on the server + */ + fun canRestoreKeys(): Boolean { + return session.cryptoService().keysBackupService().canRestoreKeys() + } + + override fun onCleared() { + super.onCleared() + session.cryptoService().keysBackupService().removeListener(this) + } + + override fun onStateChange(newState: KeysBackupState) { + keyBackupPublishSubject.onNext(session.cryptoService().keysBackupService().state) + keysBackupState.value = newState + } + + fun refreshRemoteStateIfNeeded() { + if (keysBackupState.value == KeysBackupState.Disabled) { + session.cryptoService().keysBackupService().checkAndStartKeysBackup() + } + } + + override fun handle(action: EmptyAction) {} +} diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt index e1ef7bc07b..fa5c4119ed 100644 --- a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt @@ -31,16 +31,21 @@ import androidx.core.view.isVisible import androidx.lifecycle.Observer import androidx.transition.TransitionManager import butterknife.BindView +import com.airbnb.mvrx.fragmentViewModel import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.core.utils.toast +import im.vector.riotx.features.attachments.preview.AttachmentsPreviewViewModel import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity +import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsViewModel +import javax.inject.Inject -class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() { +class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), ServerBackupStatusViewModel.Factory { @BindView(R.id.bottom_sheet_signout_warning_text) lateinit var sheetTitle: TextView @@ -84,13 +89,23 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() { isCancelable = true } - private lateinit var viewModel: SignOutViewModel + @Inject + lateinit var viewModelFactory: ServerBackupStatusViewModel.Factory + + override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel { + return viewModelFactory.create(initialState) + } + + private val viewModel: ServerBackupStatusViewModel by fragmentViewModel(ServerBackupStatusViewModel::class) + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - viewModel = fragmentViewModelProvider.get(SignOutViewModel::class.java) - setupClickableView.setOnClickListener { context?.let { context -> startActivityForResult(KeysBackupSetupActivity.intent(context, true), EXPORT_REQ) @@ -234,4 +249,5 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() { } } } + } diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutViewModel.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutViewModel.kt deleted file mode 100644 index 2f26fdf377..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutViewModel.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2019 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.riotx.features.workers.signout - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import im.vector.matrix.android.api.session.Session -import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState -import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener -import javax.inject.Inject - -class SignOutViewModel @Inject constructor(private val session: Session) : ViewModel(), KeysBackupStateListener { - // Keys exported manually - var keysExportedToFile = MutableLiveData() - - var keysBackupState = MutableLiveData() - - init { - session.cryptoService().keysBackupService().addListener(this) - - keysBackupState.value = session.cryptoService().keysBackupService().state - } - - /** - * Safe way to get the current KeysBackup version - */ - fun getCurrentBackupVersion(): String { - return session.cryptoService().keysBackupService().currentBackupVersion ?: "" - } - - /** - * Safe way to get the number of keys to backup - */ - fun getNumberOfKeysToBackup(): Int { - return session.cryptoService().inboundGroupSessionsCount(false) - } - - /** - * Safe way to tell if there are more keys on the server - */ - fun canRestoreKeys(): Boolean { - return session.cryptoService().keysBackupService().canRestoreKeys() - } - - override fun onCleared() { - super.onCleared() - - session.cryptoService().keysBackupService().removeListener(this) - } - - override fun onStateChange(newState: KeysBackupState) { - keysBackupState.value = newState - } - - fun refreshRemoteStateIfNeeded() { - if (keysBackupState.value == KeysBackupState.Disabled) { - session.cryptoService().keysBackupService().checkAndStartKeysBackup() - } - } -} diff --git a/vector/src/main/res/drawable/ic_secure_backup.xml b/vector/src/main/res/drawable/ic_secure_backup.xml new file mode 100644 index 0000000000..899bb8d2ae --- /dev/null +++ b/vector/src/main/res/drawable/ic_secure_backup.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_home_detail.xml b/vector/src/main/res/layout/fragment_home_detail.xml index f90422dff9..aa7a76cf16 100644 --- a/vector/src/main/res/layout/fragment_home_detail.xml +++ b/vector/src/main/res/layout/fragment_home_detail.xml @@ -59,6 +59,8 @@ android:layout_height="wrap_content" android:background="?riotx_keys_backup_banner_accent_color" android:minHeight="67dp" + android:visibility="gone" + tools:visibility="visible" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/syncStateView" /> diff --git a/vector/src/main/res/layout/view_keys_backup_banner.xml b/vector/src/main/res/layout/view_keys_backup_banner.xml index 87c92cf8b4..4c3ec1da3f 100644 --- a/vector/src/main/res/layout/view_keys_backup_banner.xml +++ b/vector/src/main/res/layout/view_keys_backup_banner.xml @@ -10,11 +10,11 @@ New Key Backup A new secure message key backup has been detected.\n\nIf you didn’t set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings. It was me + - Never lose encrypted messages - Start using Key Backup + Secure Backup + Safeguard against losing access to encrypted messages & data Never lose encrypted messages Use Key Backup @@ -1503,7 +1504,7 @@ Why choose Riot.im? New secure message keys Manage in Key Backup - Backing up keys… + Backing up your keys. This may take several minutes… All keys backed up From 332f227bc1fa7a7b505d428638d767c1ccdcde13 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 8 Jul 2020 09:59:13 +0200 Subject: [PATCH 20/41] Signout to setup 4S --- .../crosssigning/CrossSigningService.kt | 2 + .../DefaultCrossSigningService.kt | 7 + .../vector/riotx/core/di/ViewModelModule.kt | 1 - .../vector/riotx/core/extensions/Fragment.kt | 33 +++ .../vector/riotx/core/extensions/Session.kt | 9 + .../riotx/core/ui/views/KeysBackupBanner.kt | 3 - .../features/crypto/keys/KeysExporter.kt | 25 +- .../setup/KeysBackupSetupActivity.kt | 82 ++++-- .../setup/KeysBackupSetupStep2Fragment.kt | 8 +- .../recover/BootstrapCrossSigningTask.kt | 1 - .../recover/BootstrapSharedViewModel.kt | 5 +- .../riotx/features/home/HomeActivity.kt | 1 - .../riotx/features/home/HomeDetailFragment.kt | 2 - .../features/navigation/DefaultNavigator.kt | 1 - .../VectorSettingsSecurityPrivacyFragment.kt | 117 ++++---- .../signout/ServerBackupStatusViewModel.kt | 7 +- .../SignOutBottomSheetDialogFragment.kt | 272 +++++++++++------- .../workers/signout/SignOutUiWorker.kt | 4 +- .../signout/SignoutBottomSheetActionButton.kt | 95 ++++++ .../workers/signout/SignoutCheckViewModel.kt | 148 ++++++++++ .../layout/bottom_sheet_logout_and_backup.xml | 157 +++------- .../main/res/layout/item_signout_action.xml | 36 +++ vector/src/main/res/values/attrs.xml | 6 + vector/src/main/res/values/strings.xml | 4 + 24 files changed, 684 insertions(+), 342 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/workers/signout/SignoutBottomSheetActionButton.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/workers/signout/SignoutCheckViewModel.kt create mode 100644 vector/src/main/res/layout/item_signout_action.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt index 8d856d0860..5709e66581 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt @@ -61,6 +61,8 @@ interface CrossSigningService { fun canCrossSign(): Boolean + fun allPrivateKeysKnown(): Boolean + fun trustUser(otherUserId: String, callback: MatrixCallback) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt index 7c5f64182c..fdecfe202e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -507,6 +507,13 @@ internal class DefaultCrossSigningService @Inject constructor( && cryptoStore.getCrossSigningPrivateKeys()?.user != null } + override fun allPrivateKeysKnown(): Boolean { + return checkSelfTrust().isVerified() + && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null + && cryptoStore.getCrossSigningPrivateKeys()?.user != null + && cryptoStore.getCrossSigningPrivateKeys()?.master != null + } + override fun trustUser(otherUserId: String, callback: MatrixCallback) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { Timber.d("## CrossSigning - Mark user $userId as trusted ") diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index 2a3db0cf19..6ac6fa03da 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -36,7 +36,6 @@ import im.vector.riotx.features.reactions.EmojiChooserViewModel import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.riotx.features.roomprofile.RoomProfileSharedActionViewModel import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel -import im.vector.riotx.features.workers.signout.ServerBackupStatusViewModel @Module interface ViewModelModule { diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt index c28dcf12d3..7c1cae3644 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Fragment.kt @@ -16,9 +16,16 @@ package im.vector.riotx.core.extensions +import android.content.ActivityNotFoundException +import android.content.Intent import android.os.Parcelable import androidx.fragment.app.Fragment +import im.vector.riotx.R import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.utils.toast +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale fun VectorBaseFragment.addFragment(frameId: Int, fragment: Fragment) { parentFragmentManager.commitTransactionNow { add(frameId, fragment) } @@ -89,3 +96,29 @@ fun Fragment.getAllChildFragments(): List { // Define a missing constant const val POP_BACK_STACK_EXCLUSIVE = 0 + +fun Fragment.queryExportKeys(userId: String, requestCode: Int) { + // We need WRITE_EXTERNAL permission +// if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, +// this, +// PERMISSION_REQUEST_CODE_EXPORT_KEYS, +// R.string.permissions_rationale_msg_keys_backup_export)) { + // WRITE permissions are not needed + val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).let { + it.format(Date()) + } + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "text/plain" + intent.putExtra( + Intent.EXTRA_TITLE, + "riot-megolm-export-$userId-$timestamp.txt" + ) + + try { + startActivityForResult(Intent.createChooser(intent, getString(R.string.keys_backup_setup_step1_manual_export)), requestCode) + } catch (activityNotFoundException: ActivityNotFoundException) { + activity?.toast(R.string.error_no_external_application_found) + } +// } +} diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt index 29b169ffd4..1ad6fb9090 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt @@ -65,3 +65,12 @@ fun Session.hasUnsavedKeys(): Boolean { return cryptoService().inboundGroupSessionsCount(false) > 0 && cryptoService().keysBackupService().state != KeysBackupState.ReadyToBackUp } + +fun Session.cannotLogoutSafely(): Boolean { + // has some encrypted chat + return hasUnsavedKeys() + // has local cross signing keys + || (cryptoService().crossSigningService().allPrivateKeysKnown() + // That are not backed up + && !sharedSecretStorageService.isRecoverySetup()) +} diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt index 460c871288..252eab02a6 100755 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt @@ -21,13 +21,10 @@ import androidx.preference.PreferenceManager import android.util.AttributeSet import android.view.View import android.view.ViewGroup -import android.widget.AbsListView import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout -import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.edit import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView import androidx.transition.TransitionManager import butterknife.BindView import butterknife.ButterKnife diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt index b9b75588f1..2467334f69 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt @@ -17,37 +17,34 @@ package im.vector.riotx.features.crypto.keys import android.content.Context -import android.os.Environment +import android.net.Uri import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.util.awaitCallback -import im.vector.riotx.core.files.addEntryToDownloadManager -import im.vector.riotx.core.files.writeToFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.io.File class KeysExporter(private val session: Session) { /** * Export keys and return the file path with the callback */ - fun export(context: Context, password: String, callback: MatrixCallback) { + fun export(context: Context, password: String, uri: Uri, callback: MatrixCallback) { GlobalScope.launch(Dispatchers.Main) { runCatching { - val data = awaitCallback { session.cryptoService().exportRoomKeys(password, it) } withContext(Dispatchers.IO) { - val parentDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) - val file = File(parentDir, "riotx-keys-" + System.currentTimeMillis() + ".txt") - - writeToFile(data, file) - - addEntryToDownloadManager(context, file, "text/plain") - - file.absolutePath + val data = awaitCallback { session.cryptoService().exportRoomKeys(password, it) } + val os = context.contentResolver?.openOutputStream(uri) + if (os == null) { + false + } else { + os.write(data) + os.flush() + true + } } }.foldToCallback(callback) } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt index c7d3da30ea..1371ce7021 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt @@ -15,6 +15,8 @@ */ package im.vector.riotx.features.crypto.keysbackup.setup +import android.app.Activity +import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import androidx.appcompat.app.AlertDialog @@ -132,36 +134,16 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() { this, PERMISSION_REQUEST_CODE_EXPORT_KEYS, R.string.permissions_rationale_msg_keys_backup_export)) { - ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener { - override fun onPassphrase(passphrase: String) { - showWaitingView() + try { + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "text/plain" + intent.putExtra(Intent.EXTRA_TITLE, "riot-megolm-export-${session.myUserId}-${System.currentTimeMillis()}.txt") - KeysExporter(session) - .export(this@KeysBackupSetupActivity, - passphrase, - object : MatrixCallback { - override fun onSuccess(data: String) { - hideWaitingView() - - AlertDialog.Builder(this@KeysBackupSetupActivity) - .setMessage(getString(R.string.encryption_export_saved_as, data)) - .setCancelable(false) - .setPositiveButton(R.string.ok) { _, _ -> - val resultIntent = Intent() - resultIntent.putExtra(MANUAL_EXPORT, true) - setResult(RESULT_OK, resultIntent) - finish() - } - .show() - } - - override fun onFailure(failure: Throwable) { - toast(failure.localizedMessage ?: getString(R.string.unexpected_error)) - hideWaitingView() - } - }) - } - }) + startActivityForResult(Intent.createChooser(intent, getString(R.string.keys_backup_setup_step1_manual_export)), REQUEST_CODE_SAVE_MEGOLM_EXPORT) + } catch (activityNotFoundException: ActivityNotFoundException) { + toast(R.string.error_no_external_application_found) + } } } @@ -173,6 +155,47 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() { } } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == REQUEST_CODE_SAVE_MEGOLM_EXPORT) { + val uri = data?.data + if (resultCode == Activity.RESULT_OK && uri != null) { + ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener { + override fun onPassphrase(passphrase: String) { + showWaitingView() + + KeysExporter(session) + .export(this@KeysBackupSetupActivity, + passphrase, + uri, + object : MatrixCallback { + override fun onSuccess(data: Boolean) { + if (data) { + toast(getString(R.string.encryption_exported_successfully)) + Intent().apply { + putExtra(MANUAL_EXPORT, true) + }.let { + setResult(Activity.RESULT_OK, it) + finish() + } + } + hideWaitingView() + } + + override fun onFailure(failure: Throwable) { + toast(failure.localizedMessage ?: getString(R.string.unexpected_error)) + hideWaitingView() + } + }) + } + }) + } else { + toast(getString(R.string.unexpected_error)) + hideWaitingView() + } + } + super.onActivityResult(requestCode, resultCode, data) + } + override fun onBackPressed() { if (viewModel.shouldPromptOnBack) { if (waitingView?.isVisible == true) { @@ -205,6 +228,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() { const val KEYS_VERSION = "KEYS_VERSION" const val MANUAL_EXPORT = "MANUAL_EXPORT" const val EXTRA_SHOW_MANUAL_EXPORT = "SHOW_MANUAL_EXPORT" + const val REQUEST_CODE_SAVE_MEGOLM_EXPORT = 101 fun intent(context: Context, showManualExport: Boolean): Intent { val intent = Intent(context, KeysBackupSetupActivity::class.java) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt index a3306677fe..40ea79eb6d 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt @@ -15,13 +15,13 @@ */ package im.vector.riotx.features.crypto.keysbackup.setup -import android.os.AsyncTask import android.os.Bundle import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.widget.EditText import android.widget.ImageView import androidx.lifecycle.Observer +import androidx.lifecycle.viewModelScope import androidx.transition.TransitionManager import butterknife.BindView import butterknife.OnClick @@ -33,6 +33,8 @@ import im.vector.riotx.core.extensions.showPassword import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.ui.views.PasswordStrengthBar import im.vector.riotx.features.settings.VectorLocale +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import javax.inject.Inject class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment() { @@ -117,9 +119,9 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment() if (newValue.isEmpty()) { viewModel.passwordStrength.value = null } else { - AsyncTask.execute { + viewModel.viewModelScope.launch(Dispatchers.IO) { val strength = zxcvbn.measure(newValue) - activity?.runOnUiThread { + launch(Dispatchers.Main) { viewModel.passwordStrength.value = strength } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt index 290a08bfad..9f68e09444 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -168,7 +168,6 @@ class BootstrapCrossSigningTask @Inject constructor( return BootstrapResult.FailedToSetDefaultSSSSKey(failure) } - Timber.d("## BootstrapCrossSigningTask: Creating 4S - gathering private keys") val xKeys = crossSigningService.getCrossSigningPrivateKeys() val mskPrivateKey = xKeys?.master ?: return BootstrapResult.MissingPrivateKey diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt index 3a95a575f4..22dcab217e 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt @@ -406,7 +406,10 @@ class BootstrapSharedViewModel @AssistedInject constructor( setState { copy( recoveryKeyCreationInfo = bootstrapResult.keyInfo, - step = BootstrapStep.SaveRecoveryKey(false) + step = BootstrapStep.SaveRecoveryKey( + // If a passphrase was used, saving key is optional + state.passphrase != null + ) ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index 6991aaa493..ef0757be21 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -68,7 +68,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet private val homeActivityViewModel: HomeActivityViewModel by viewModel() @Inject lateinit var viewModelFactory: HomeActivityViewModel.Factory - private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel() @Inject lateinit var serverBackupviewModelFactory: ServerBackupStatusViewModel.Factory diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index c736c0c1ca..f0fdc207f9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -21,7 +21,6 @@ import android.view.LayoutInflater import android.view.View import androidx.core.content.ContextCompat import androidx.core.view.forEachIndexed -import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel @@ -194,7 +193,6 @@ class HomeDetailFragment @Inject constructor( } private fun setupKeysBackupBanner() { - serverBackupStatusViewModel.subscribe(this) { when (val banState = it.bannerState.invoke()) { is BannerState.Setup -> homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false) diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 79ba5121fc..006af48133 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -207,7 +207,6 @@ class DefaultNavigator @Inject constructor( context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport)) } } - } override fun openKeysBackupManager(context: Context) { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt index 2b9338ccc8..3c2acb1693 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -34,16 +34,16 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.riotx.R +import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.dialogs.ExportKeysDialog +import im.vector.riotx.core.extensions.queryExportKeys import im.vector.riotx.core.intent.ExternalIntentData import im.vector.riotx.core.intent.analyseIntent import im.vector.riotx.core.intent.getFilenameFromUri import im.vector.riotx.core.platform.SimpleTextWatcher import im.vector.riotx.core.preference.VectorPreference -import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_EXPORT_KEYS import im.vector.riotx.core.utils.allGranted -import im.vector.riotx.core.utils.checkPermissions import im.vector.riotx.core.utils.openFileSelection import im.vector.riotx.core.utils.toast import im.vector.riotx.features.crypto.keys.KeysExporter @@ -52,7 +52,8 @@ import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActiv import javax.inject.Inject class VectorSettingsSecurityPrivacyFragment @Inject constructor( - private val vectorPreferences: VectorPreferences + private val vectorPreferences: VectorPreferences, + private val activeSessionHolder: ActiveSessionHolder ) : VectorSettingsBaseFragment() { override var titleRes = R.string.settings_security_and_privacy @@ -119,38 +120,69 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( } private fun refreshXSigningStatus() { - val xSigningIsEnableInAccount = session.cryptoService().crossSigningService().isCrossSigningInitialized() - val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified() - val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign() + val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys() + val xSigningIsEnableInAccount = crossSigningKeys != null + val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified() + val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign() - if (xSigningKeyCanSign) { - mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted) - mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete) - } else if (xSigningKeysAreTrusted) { - mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom) - mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted) - } else if (xSigningIsEnableInAccount) { - mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black) - mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted) - } else { - mCrossSigningStatePreference.setIcon(android.R.color.transparent) - mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_disabled) - } + if (xSigningKeyCanSign) { + mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted) + mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete) + } else if (xSigningKeysAreTrusted) { + mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom) + mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted) + } else if (xSigningIsEnableInAccount) { + mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black) + mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted) + } else { + mCrossSigningStatePreference.setIcon(android.R.color.transparent) + mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_disabled) + } - mCrossSigningStatePreference.isVisible = true + mCrossSigningStatePreference.isVisible = true } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { if (allGranted(grantResults)) { if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) { - exportKeys() + queryExportKeys(activeSessionHolder.getSafeActiveSession()?.myUserId ?: "", REQUEST_CODE_SAVE_MEGOLM_EXPORT) } } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQUEST_CODE_SAVE_MEGOLM_EXPORT) { + val uri = data?.data + if (resultCode == Activity.RESULT_OK && uri != null) { + activity?.let { activity -> + ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener { + override fun onPassphrase(passphrase: String) { + displayLoadingView() + KeysExporter(session) + .export(requireContext(), + passphrase, + uri, + object : MatrixCallback { + override fun onSuccess(data: Boolean) { + if (data) { + requireActivity().toast(getString(R.string.encryption_exported_successfully)) + } else { + requireActivity().toast(getString(R.string.unexpected_error)) + } + hideLoadingView() + } + + override fun onFailure(failure: Throwable) { + onCommonDone(failure.localizedMessage) + } + }) + } + }) + } + } + } if (resultCode == Activity.RESULT_OK) { when (requestCode) { REQUEST_E2E_FILE_REQUEST_CODE -> importKeys(data) @@ -169,7 +201,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( } exportPref.onPreferenceClickListener = Preference.OnPreferenceClickListener { - exportKeys() + queryExportKeys(activeSessionHolder.getSafeActiveSession()?.myUserId ?: "", REQUEST_CODE_SAVE_MEGOLM_EXPORT) true } @@ -179,46 +211,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( } } - /** - * Manage the e2e keys export. - */ - private fun exportKeys() { - // We need WRITE_EXTERNAL permission - if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, - this, - PERMISSION_REQUEST_CODE_EXPORT_KEYS, - R.string.permissions_rationale_msg_keys_backup_export)) { - activity?.let { activity -> - ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener { - override fun onPassphrase(passphrase: String) { - displayLoadingView() - - KeysExporter(session) - .export(requireContext(), - passphrase, - object : MatrixCallback { - override fun onSuccess(data: String) { - if (isAdded) { - hideLoadingView() - - AlertDialog.Builder(activity) - .setMessage(getString(R.string.encryption_export_saved_as, data)) - .setCancelable(false) - .setPositiveButton(R.string.ok, null) - .show() - } - } - - override fun onFailure(failure: Throwable) { - onCommonDone(failure.localizedMessage) - } - }) - } - }) - } - } - } - /** * Manage the e2e keys import. */ @@ -515,6 +507,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( companion object { private const val REQUEST_E2E_FILE_REQUEST_CODE = 123 + private const val REQUEST_CODE_SAVE_MEGOLM_EXPORT = 124 private const val PUSHER_PREFERENCE_KEY_BASE = "PUSHER_PREFERENCE_KEY_BASE" private const val DEVICES_PREFERENCE_KEY_BASE = "DEVICES_PREFERENCE_KEY_BASE" diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt index 04ece8e407..dca98c16b2 100644 --- a/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/ServerBackupStatusViewModel.kt @@ -94,9 +94,8 @@ class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialS init { session.cryptoService().keysBackupService().addListener(this) - keyBackupPublishSubject.onNext(session.cryptoService().keysBackupService().state) keysBackupState.value = session.cryptoService().keysBackupService().state - session.rx().liveCrossSigningPrivateKeys() + Observable.combineLatest, Optional, KeysBackupState, Optional, BannerState>( session.rx().liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME)), session.rx().liveCrossSigningInfo(session.myUserId), @@ -126,13 +125,15 @@ class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialS BannerState.Hidden } ) - .throttleLast(2000, TimeUnit.MILLISECONDS) // we don't want to flicker or catch transient states + .throttleLast(1000, TimeUnit.MILLISECONDS) // we don't want to flicker or catch transient states .distinctUntilChanged() .execute { async -> copy( bannerState = async ) } + + keyBackupPublishSubject.onNext(session.cryptoService().keysBackupService().state) } /** diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt index fa5c4119ed..16be661f06 100644 --- a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt @@ -28,24 +28,27 @@ import android.widget.ProgressBar import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible -import androidx.lifecycle.Observer -import androidx.transition.TransitionManager import butterknife.BindView +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog +import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.dialogs.ExportKeysDialog +import im.vector.riotx.core.extensions.queryExportKeys import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment -import im.vector.riotx.core.utils.toast -import im.vector.riotx.features.attachments.preview.AttachmentsPreviewViewModel -import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity -import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsViewModel +import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet +import timber.log.Timber import javax.inject.Inject -class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), ServerBackupStatusViewModel.Factory { +// TODO this needs to be refactored to current standard and remove legacy +class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), SignoutCheckViewModel.Factory { @BindView(R.id.bottom_sheet_signout_warning_text) lateinit var sheetTitle: TextView @@ -53,14 +56,20 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), @BindView(R.id.bottom_sheet_signout_backingup_status_group) lateinit var backingUpStatusGroup: ViewGroup - @BindView(R.id.keys_backup_setup) - lateinit var setupClickableView: View + @BindView(R.id.setupRecoveryButton) + lateinit var setupRecoveryButton: SignoutBottomSheetActionButton - @BindView(R.id.keys_backup_activate) - lateinit var activateClickableView: View + @BindView(R.id.setupMegolmBackupButton) + lateinit var setupMegolmBackupButton: SignoutBottomSheetActionButton - @BindView(R.id.keys_backup_dont_want) - lateinit var dontWantClickableView: View + @BindView(R.id.exportManuallyButton) + lateinit var exportManuallyButton: SignoutBottomSheetActionButton + + @BindView(R.id.exitAnywayButton) + lateinit var exitAnywayButton: SignoutBottomSheetActionButton + + @BindView(R.id.signOutButton) + lateinit var signOutButton: SignoutBottomSheetActionButton @BindView(R.id.bottom_sheet_signout_icon_progress_bar) lateinit var backupProgress: ProgressBar @@ -71,8 +80,8 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), @BindView(R.id.bottom_sheet_backup_status_text) lateinit var backupStatusTex: TextView - @BindView(R.id.bottom_sheet_signout_button) - lateinit var signoutClickableView: View + @BindView(R.id.signoutExportingLoading) + lateinit var signoutExportingLoading: View @BindView(R.id.root_layout) lateinit var rootLayout: ViewGroup @@ -83,6 +92,7 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), fun newInstance() = SignOutBottomSheetDialogFragment() private const val EXPORT_REQ = 0 + private const val QUERY_EXPORT_KEYS = 1 } init { @@ -90,65 +100,36 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), } @Inject - lateinit var viewModelFactory: ServerBackupStatusViewModel.Factory + lateinit var viewModelFactory: SignoutCheckViewModel.Factory - override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel { + override fun create(initialState: SignoutCheckViewState): SignoutCheckViewModel { return viewModelFactory.create(initialState) } - private val viewModel: ServerBackupStatusViewModel by fragmentViewModel(ServerBackupStatusViewModel::class) + private val viewModel: SignoutCheckViewModel by fragmentViewModel(SignoutCheckViewModel::class) override fun injectWith(injector: ScreenComponent) { injector.inject(this) } + override fun onResume() { + super.onResume() + viewModel.refreshRemoteStateIfNeeded() + } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - setupClickableView.setOnClickListener { - context?.let { context -> - startActivityForResult(KeysBackupSetupActivity.intent(context, true), EXPORT_REQ) - } + setupRecoveryButton.action = { + BootstrapBottomSheet.show(parentFragmentManager, false) } - activateClickableView.setOnClickListener { - context?.let { context -> - startActivity(KeysBackupManageActivity.intent(context)) - } - } - - signoutClickableView.setOnClickListener { - this.onSignOut?.run() - } - - dontWantClickableView.setOnClickListener { _ -> + exitAnywayButton.action = { context?.let { AlertDialog.Builder(it) .setTitle(R.string.are_you_sure) .setMessage(R.string.sign_out_bottom_sheet_will_lose_secure_messages) - .setPositiveButton(R.string.backup) { _, _ -> - when (viewModel.keysBackupState.value) { - KeysBackupState.NotTrusted -> { - context?.let { context -> - startActivity(KeysBackupManageActivity.intent(context)) - } - } - KeysBackupState.Disabled -> { - context?.let { context -> - startActivityForResult(KeysBackupSetupActivity.intent(context, true), EXPORT_REQ) - } - } - KeysBackupState.BackingUp, - KeysBackupState.WillBackUp -> { - // keys are already backing up please wait - context?.toast(R.string.keys_backup_is_not_finished_please_wait) - } - else -> { - // nop - } - } - } + .setPositiveButton(R.string.backup, null) .setNegativeButton(R.string.action_sign_out) { _, _ -> onSignOut?.run() } @@ -156,71 +137,143 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), } } - viewModel.keysExportedToFile.observe(viewLifecycleOwner, Observer { - val hasExportedToFile = it ?: false - if (hasExportedToFile) { - // We can allow to sign out - - sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple) - - signoutClickableView.isVisible = true - dontWantClickableView.isVisible = false - setupClickableView.isVisible = false - activateClickableView.isVisible = false - backingUpStatusGroup.isVisible = false + exportManuallyButton.action = { + withState(viewModel) { state -> + queryExportKeys(state.userId, QUERY_EXPORT_KEYS) } - }) + } - viewModel.keysBackupState.observe(viewLifecycleOwner, Observer { - if (viewModel.keysExportedToFile.value == true) { - // ignore this - return@Observer - } - TransitionManager.beginDelayedTransition(rootLayout) + setupMegolmBackupButton.action = { + startActivityForResult(KeysBackupSetupActivity.intent(requireContext(), true), EXPORT_REQ) + } + + viewModel.observeViewEvents { when (it) { - KeysBackupState.ReadyToBackUp -> { - signoutClickableView.isVisible = true - dontWantClickableView.isVisible = false - setupClickableView.isVisible = false - activateClickableView.isVisible = false - backingUpStatusGroup.isVisible = true + is SignoutCheckViewModel.ViewEvents.ExportKeys -> { + it.exporter + .export(requireContext(), + it.passphrase, + it.uri, + object : MatrixCallback { + override fun onSuccess(data: Boolean) { + if (data) { + viewModel.handle(SignoutCheckViewModel.Actions.KeySuccessfullyManuallyExported) + } else { + viewModel.handle(SignoutCheckViewModel.Actions.KeyExportFailed) + } + } + override fun onFailure(failure: Throwable) { + Timber.e("## Failed to export manually keys ${failure.localizedMessage}") + viewModel.handle(SignoutCheckViewModel.Actions.KeyExportFailed) + } + }) + } + } + } + } + + override fun invalidate() = withState(viewModel) { state -> + signoutExportingLoading.isVisible = false + if (state.crossSigningSetupAllKeysKnown && !state.backupIsSetup) { + sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_no_backup) + backingUpStatusGroup.isVisible = false + // we should show option to setup 4S + setupRecoveryButton.isVisible = true + setupMegolmBackupButton.isVisible = false + signOutButton.isVisible = false + // We let the option to ignore and quit + exportManuallyButton.isVisible = true + exitAnywayButton.isVisible = true + } else if (state.keysBackupState == KeysBackupState.Unknown || state.keysBackupState == KeysBackupState.Disabled) { + sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_no_backup) + backingUpStatusGroup.isVisible = false + // no key backup and cannot setup full 4S + // we propose to setup + // we should show option to setup 4S + setupRecoveryButton.isVisible = false + setupMegolmBackupButton.isVisible = true + signOutButton.isVisible = false + // We let the option to ignore and quit + exportManuallyButton.isVisible = true + exitAnywayButton.isVisible = true + } else { + // so keybackup is setup + // You should wait until all are uploaded + setupRecoveryButton.isVisible = false + + when (state.keysBackupState) { + KeysBackupState.ReadyToBackUp -> { + sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple) + + // Ok all keys are backedUp + backingUpStatusGroup.isVisible = true backupProgress.isVisible = false backupCompleteImage.isVisible = true backupStatusTex.text = getString(R.string.keys_backup_info_keys_all_backup_up) - sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple) + hideViews(setupMegolmBackupButton, exportManuallyButton, exitAnywayButton) + // You can signout + signOutButton.isVisible = true } - KeysBackupState.BackingUp, - KeysBackupState.WillBackUp -> { - backingUpStatusGroup.isVisible = true - sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_backing_up) - dontWantClickableView.isVisible = true - setupClickableView.isVisible = false - activateClickableView.isVisible = false + KeysBackupState.WillBackUp, + KeysBackupState.BackingUp -> { + sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_backing_up) + + // save in progress + backingUpStatusGroup.isVisible = true backupProgress.isVisible = true backupCompleteImage.isVisible = false backupStatusTex.text = getString(R.string.sign_out_bottom_sheet_backing_up_keys) + + hideViews(setupMegolmBackupButton, setupMegolmBackupButton, signOutButton, exportManuallyButton) + exitAnywayButton.isVisible = true } KeysBackupState.NotTrusted -> { - backingUpStatusGroup.isVisible = false - dontWantClickableView.isVisible = true - setupClickableView.isVisible = false - activateClickableView.isVisible = true sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_backup_not_active) + // It's not trusted and we know there are unsaved keys.. + backingUpStatusGroup.isVisible = false + + exportManuallyButton.isVisible = true + // option to enter pass/key + setupMegolmBackupButton.isVisible = true + exitAnywayButton.isVisible = true } else -> { - backingUpStatusGroup.isVisible = false - dontWantClickableView.isVisible = true - setupClickableView.isVisible = true - activateClickableView.isVisible = false - sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_no_backup) + // mmm.. strange state + + exitAnywayButton.isVisible = true } } + } - // updateSignOutSection() - }) + // final call if keys have been exported + when (state.hasBeenExportedToFile) { + is Loading -> { + signoutExportingLoading.isVisible = true + hideViews(setupRecoveryButton, + setupMegolmBackupButton, + exportManuallyButton, + backingUpStatusGroup, + signOutButton) + exitAnywayButton.isVisible = true + } + is Success -> { + if (state.hasBeenExportedToFile.invoke()) { + sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple) + hideViews(setupRecoveryButton, + setupMegolmBackupButton, + exportManuallyButton, + backingUpStatusGroup, + exitAnywayButton) + signOutButton.isVisible = true + } + } + else -> { + } + } + super.invalidate() } override fun getLayoutResId() = R.layout.bottom_sheet_logout_and_backup @@ -243,11 +296,26 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), super.onActivityResult(requestCode, resultCode, data) if (resultCode == Activity.RESULT_OK) { - if (requestCode == EXPORT_REQ) { - val manualExportDone = data?.getBooleanExtra(KeysBackupSetupActivity.MANUAL_EXPORT, false) - viewModel.keysExportedToFile.value = manualExportDone + if (requestCode == QUERY_EXPORT_KEYS) { + val uri = data?.data + if (resultCode == Activity.RESULT_OK && uri != null) { + activity?.let { activity -> + ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener { + override fun onPassphrase(passphrase: String) { + viewModel.handle(SignoutCheckViewModel.Actions.ExportKeys(passphrase, uri)) + } + }) + } + } + } else if (requestCode == EXPORT_REQ) { + if (data?.getBooleanExtra(KeysBackupSetupActivity.MANUAL_EXPORT, false) == true) { + viewModel.handle(SignoutCheckViewModel.Actions.KeySuccessfullyManuallyExported) + } } } } + private fun hideViews(vararg views: View) { + views.forEach { it.isVisible = false } + } } diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutUiWorker.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutUiWorker.kt index e51fda2be5..e06a47d3d4 100644 --- a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutUiWorker.kt +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutUiWorker.kt @@ -21,7 +21,7 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.FragmentActivity import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder -import im.vector.riotx.core.extensions.hasUnsavedKeys +import im.vector.riotx.core.extensions.cannotLogoutSafely import im.vector.riotx.core.extensions.vectorComponent import im.vector.riotx.features.MainActivity import im.vector.riotx.features.MainActivityArgs @@ -33,7 +33,7 @@ class SignOutUiWorker(private val activity: FragmentActivity) { fun perform(context: Context) { activeSessionHolder = context.vectorComponent().activeSessionHolder() val session = activeSessionHolder.getActiveSession() - if (session.hasUnsavedKeys()) { + if (session.cannotLogoutSafely()) { // The backup check on logout flow has to be displayed if there are keys in the store, and the keys backup state is not Ready val signOutDialog = SignOutBottomSheetDialogFragment.newInstance() signOutDialog.onSignOut = Runnable { diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignoutBottomSheetActionButton.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignoutBottomSheetActionButton.kt new file mode 100644 index 0000000000..cd5e4ed9da --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignoutBottomSheetActionButton.kt @@ -0,0 +1,95 @@ +/* + * 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.riotx.features.workers.signout + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.view.isVisible +import butterknife.BindView +import butterknife.ButterKnife +import im.vector.riotx.R +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.features.themes.ThemeUtils + +class SignoutBottomSheetActionButton @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + @BindView(R.id.actionTitleText) + lateinit var actionTextView: TextView + + @BindView(R.id.actionIconImageView) + lateinit var iconImageView: ImageView + + @BindView(R.id.signedOutActionClickable) + lateinit var clickableZone: View + + var action: (() -> Unit)? = null + + var title: String? = null + set(value) { + field = value + actionTextView.setTextOrHide(value) + } + + var leftIcon: Drawable? = null + set(value) { + field = value + if (value == null) { + iconImageView.isVisible = false + iconImageView.setImageDrawable(null) + } else { + iconImageView.isVisible = true + iconImageView.setImageDrawable(value) + } + } + + var tint: Int? = null + set(value) { + field = value + iconImageView.imageTintList = value?.let { ColorStateList.valueOf(value) } + } + + var textColor: Int? = null + set(value) { + field = value + textColor?.let { actionTextView.setTextColor(it) } + } + + init { + inflate(context, R.layout.item_signout_action, this) + ButterKnife.bind(this) + + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.SignoutBottomSheetActionButton, 0, 0) + title = typedArray.getString(R.styleable.SignoutBottomSheetActionButton_actionTitle) ?: "" + leftIcon = typedArray.getDrawable(R.styleable.SignoutBottomSheetActionButton_leftIcon) + tint = typedArray.getColor(R.styleable.SignoutBottomSheetActionButton_iconTint, ThemeUtils.getColor(context, android.R.attr.textColor)) + textColor = typedArray.getColor(R.styleable.SignoutBottomSheetActionButton_textColor, ThemeUtils.getColor(context, android.R.attr.textColor)) + + typedArray.recycle() + + clickableZone.setOnClickListener { + action?.invoke() + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignoutCheckViewModel.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignoutCheckViewModel.kt new file mode 100644 index 0000000000..47da7d4edc --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignoutCheckViewModel.kt @@ -0,0 +1,148 @@ +/* + * 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.riotx.features.workers.signout + +import android.net.Uri +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener +import im.vector.matrix.rx.rx +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.platform.VectorViewEvents +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.platform.VectorViewModelAction +import im.vector.riotx.features.crypto.keys.KeysExporter + +data class SignoutCheckViewState( + val userId: String = "", + val backupIsSetup: Boolean = false, + val crossSigningSetupAllKeysKnown: Boolean = false, + val keysBackupState: KeysBackupState = KeysBackupState.Unknown, + val hasBeenExportedToFile: Async = Uninitialized +) : MvRxState + +class SignoutCheckViewModel @AssistedInject constructor(@Assisted initialState: SignoutCheckViewState, + private val session: Session) + : VectorViewModel(initialState), KeysBackupStateListener { + + sealed class Actions : VectorViewModelAction { + data class ExportKeys(val passphrase: String, val uri: Uri) : Actions() + object KeySuccessfullyManuallyExported : Actions() + object KeyExportFailed : Actions() + } + + sealed class ViewEvents : VectorViewEvents { + data class ExportKeys(val exporter: KeysExporter, val passphrase: String, val uri: Uri) : ViewEvents() + } + + @AssistedInject.Factory + interface Factory { + fun create(initialState: SignoutCheckViewState): SignoutCheckViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: SignoutCheckViewState): SignoutCheckViewModel? { + 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") + } + } + + init { + session.cryptoService().keysBackupService().addListener(this) + session.cryptoService().keysBackupService().checkAndStartKeysBackup() + + val quad4SIsSetup = session.sharedSecretStorageService.isRecoverySetup() + val allKeysKnown = session.cryptoService().crossSigningService().allPrivateKeysKnown() + val backupState = session.cryptoService().keysBackupService().state + setState { + copy( + userId = session.myUserId, + crossSigningSetupAllKeysKnown = allKeysKnown, + backupIsSetup = quad4SIsSetup, + keysBackupState = backupState + ) + } + + session.rx().liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME)) + .map { + session.sharedSecretStorageService.isRecoverySetup() + } + .distinctUntilChanged() + .execute { + copy(backupIsSetup = it.invoke() == true) + } + } + + override fun onCleared() { + super.onCleared() + session.cryptoService().keysBackupService().removeListener(this) + } + + override fun onStateChange(newState: KeysBackupState) { + setState { + copy( + keysBackupState = newState + ) + } + } + + fun refreshRemoteStateIfNeeded() = withState { state -> + if (state.keysBackupState == KeysBackupState.Disabled) { + session.cryptoService().keysBackupService().checkAndStartKeysBackup() + } + } + + override fun handle(action: Actions) { + when (action) { + is Actions.ExportKeys -> { + setState { + copy(hasBeenExportedToFile = Loading()) + } + _viewEvents.post(ViewEvents.ExportKeys(KeysExporter(session), action.passphrase, action.uri)) + } + Actions.KeySuccessfullyManuallyExported -> { + setState { + copy(hasBeenExportedToFile = Success(true)) + } + } + Actions.KeyExportFailed -> { + setState { + copy(hasBeenExportedToFile = Uninitialized) + } + } + }.exhaustive + } +} diff --git a/vector/src/main/res/layout/bottom_sheet_logout_and_backup.xml b/vector/src/main/res/layout/bottom_sheet_logout_and_backup.xml index feaa79e1dc..c6605dfc05 100644 --- a/vector/src/main/res/layout/bottom_sheet_logout_and_backup.xml +++ b/vector/src/main/res/layout/bottom_sheet_logout_and_backup.xml @@ -70,137 +70,60 @@ + android:layout_height="44dp" + android:gravity="center"> - - - - + android:layout_height="wrap_content" /> - - - - - - - + app:actionTitle="@string/secure_backup_setup" + app:iconTint="?riotx_text_primary" + app:leftIcon="@drawable/ic_secure_backup" + app:textColor="?riotx_text_secondary" /> - + app:actionTitle="@string/keys_backup_setup" + app:iconTint="?riotx_text_primary" + app:leftIcon="@drawable/backup_keys" + app:textColor="?riotx_text_secondary" /> - - - - - - + app:actionTitle="@string/keys_backup_setup_step1_manual_export" + app:iconTint="?riotx_text_primary" + app:leftIcon="@drawable/ic_download" + app:textColor="?riotx_text_secondary" /> - - - - + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_signout_action.xml b/vector/src/main/res/layout/item_signout_action.xml new file mode 100644 index 0000000000..c5acc09e56 --- /dev/null +++ b/vector/src/main/res/layout/item_signout_action.xml @@ -0,0 +1,36 @@ + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/attrs.xml b/vector/src/main/res/values/attrs.xml index 27d53fe90e..80ecf32029 100644 --- a/vector/src/main/res/values/attrs.xml +++ b/vector/src/main/res/values/attrs.xml @@ -114,4 +114,10 @@ + + + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 01386dde56..8451191273 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1048,6 +1048,7 @@ Export Please create a passphrase to encrypt the exported keys. You will need to enter the same passphrase to be able to import the keys. The E2E room keys have been saved to \'%s\'.\n\nWarning: this file may be deleted if the application is uninstalled. + Keys successfully exported Encrypted Messages Recovery Manage Key Backup @@ -1506,6 +1507,9 @@ Why choose Riot.im? Backing up your keys. This may take several minutes… + + Set Up Secure Backup + All keys backed up From 0c2516ccf8390d7cc0db72df601a2e4c259a8234 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 9 Jul 2020 15:47:59 +0200 Subject: [PATCH 21/41] line too long --- .../crypto/keysbackup/setup/KeysBackupSetupActivity.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt index 1371ce7021..b99c0e4330 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt @@ -140,7 +140,13 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() { intent.type = "text/plain" intent.putExtra(Intent.EXTRA_TITLE, "riot-megolm-export-${session.myUserId}-${System.currentTimeMillis()}.txt") - startActivityForResult(Intent.createChooser(intent, getString(R.string.keys_backup_setup_step1_manual_export)), REQUEST_CODE_SAVE_MEGOLM_EXPORT) + startActivityForResult( + Intent.createChooser( + intent, + getString(R.string.keys_backup_setup_step1_manual_export) + ), + REQUEST_CODE_SAVE_MEGOLM_EXPORT + ) } catch (activityNotFoundException: ActivityNotFoundException) { toast(R.string.error_no_external_application_found) } @@ -172,7 +178,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() { if (data) { toast(getString(R.string.encryption_exported_successfully)) Intent().apply { - putExtra(MANUAL_EXPORT, true) + putExtra(MANUAL_EXPORT, true) }.let { setResult(Activity.RESULT_OK, it) finish() From 28869f4382d4ef369cf3cb13e8a730443a4b63e4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 10 Jul 2020 12:19:25 +0200 Subject: [PATCH 22/41] Small cleanup before merge --- .../crypto/verification/DefaultVerificationService.kt | 2 +- .../vector/riotx/features/navigation/DefaultNavigator.kt | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt index dcee12c165..4d4eeb21fd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt @@ -1234,7 +1234,7 @@ internal class DefaultVerificationService @Inject constructor( ) // We can SCAN or SHOW QR codes only if cross-signing is enabled - val methodValues = if (crossSigningService.getMyCrossSigningKeys() != null) { + val methodValues = if (crossSigningService.isCrossSigningInitialized()) { // Add reciprocate method if application declares it can scan or show QR codes // Not sure if it ok to do that (?) val reciprocateMethod = methods diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 6907544252..83f0baa12c 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -118,9 +118,10 @@ class DefaultNavigator @Inject constructor( override fun requestSelfSessionVerification(context: Context) { val session = sessionHolder.getSafeActiveSession() ?: return - val otherSessions = session.cryptoService().getCryptoDeviceInfo(session.myUserId).filter { - it.deviceId != session.sessionParams.deviceId - }.map { it.deviceId } + val otherSessions = session.cryptoService() + .getCryptoDeviceInfo(session.myUserId) + .filter { it.deviceId != session.sessionParams.deviceId } + .map { it.deviceId } if (context is VectorBaseActivity) { if (otherSessions.isNotEmpty()) { val pr = session.cryptoService().verificationService().requestKeyVerification( From eff08955f147faf809dea6df30cfa88c970047f9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 10 Jul 2020 12:37:48 +0200 Subject: [PATCH 23/41] Fix a11y --- vector/src/main/res/layout/merge_image_attachment_overlay.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/res/layout/merge_image_attachment_overlay.xml b/vector/src/main/res/layout/merge_image_attachment_overlay.xml index db22c0112c..6188ad564a 100644 --- a/vector/src/main/res/layout/merge_image_attachment_overlay.xml +++ b/vector/src/main/res/layout/merge_image_attachment_overlay.xml @@ -24,7 +24,7 @@ android:layout_marginEnd="16dp" android:background="?attr/selectableItemBackgroundBorderless" android:clickable="true" - android:contentDescription="@string/share" + android:contentDescription="@string/action_close" android:focusable="true" android:padding="6dp" android:scaleType="centerInside" From e979bee9206b6100b0f3d112d3deff94a4210bb0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 10 Jul 2020 12:37:59 +0200 Subject: [PATCH 24/41] Format --- .../riotx/features/navigation/DefaultNavigator.kt | 3 ++- .../im/vector/riotx/features/navigation/Navigator.kt | 3 ++- .../res/layout/merge_image_attachment_overlay.xml | 12 ++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 8940ac6791..6acd041ecf 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -289,7 +289,8 @@ class DefaultNavigator @Inject constructor( } override fun openVideoViewer(activity: Activity, - roomId: String, mediaData: VideoContentRenderer.Data, + roomId: String, + mediaData: AttachmentData, view: View, inMemory: List?, options: ((MutableList>) -> Unit)?) { diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index f925344570..6d036f1468 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -99,7 +99,8 @@ interface Navigator { options: ((MutableList>) -> Unit)?) fun openVideoViewer(activity: Activity, - roomId: String, mediaData: VideoContentRenderer.Data, + roomId: String, + mediaData: VideoContentRenderer.Data, view: View, inMemory: List? = null, options: ((MutableList>) -> Unit)?) diff --git a/vector/src/main/res/layout/merge_image_attachment_overlay.xml b/vector/src/main/res/layout/merge_image_attachment_overlay.xml index 6188ad564a..b0e769579c 100644 --- a/vector/src/main/res/layout/merge_image_attachment_overlay.xml +++ b/vector/src/main/res/layout/merge_image_attachment_overlay.xml @@ -74,8 +74,8 @@ android:layout_marginEnd="16dp" android:background="?attr/selectableItemBackgroundBorderless" android:clickable="true" - android:focusable="true" android:contentDescription="@string/share" + android:focusable="true" android:padding="6dp" android:tint="@color/white" app:layout_constraintBottom_toBottomOf="@id/overlayTopBackground" @@ -85,12 +85,12 @@ + app:constraint_referenced_ids="overlayBottomBackground,overlayBackButton,overlayPlayPauseButton,overlaySeekBar" + tools:visibility="visible" /> Date: Fri, 10 Jul 2020 12:48:35 +0200 Subject: [PATCH 25/41] Cleanup Navigator --- .../home/room/detail/RoomDetailFragment.kt | 16 ++++-- .../media/VectorAttachmentViewerActivity.kt | 5 +- .../features/navigation/DefaultNavigator.kt | 52 +------------------ .../riotx/features/navigation/Navigator.kt | 11 +--- .../uploads/media/RoomUploadsMediaFragment.kt | 19 +++++-- 5 files changed, 34 insertions(+), 69 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index a457587aa8..938ae6a1bb 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -1171,14 +1171,24 @@ class RoomDetailFragment @Inject constructor( } override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) { - navigator.openImageViewer(requireActivity(), roomDetailArgs.roomId, mediaData, view) { pairs -> + navigator.openMediaViewer( + activity = requireActivity(), + roomId = roomDetailArgs.roomId, + mediaData = mediaData, + view = view + ) { pairs -> pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: "")) pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: "")) } } override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) { - navigator.openVideoViewer(requireActivity(), roomDetailArgs.roomId, mediaData, view) { pairs -> + navigator.openMediaViewer( + activity = requireActivity(), + roomId = roomDetailArgs.roomId, + mediaData = mediaData, + view = view + ) { pairs -> pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: "")) pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: "")) } @@ -1199,7 +1209,7 @@ class RoomDetailFragment @Inject constructor( override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { if (allGranted(grantResults)) { when (requestCode) { - SAVE_ATTACHEMENT_REQUEST_CODE -> { + SAVE_ATTACHEMENT_REQUEST_CODE -> { sharedActionViewModel.pendingAction?.let { handleActions(it) sharedActionViewModel.pendingAction = null diff --git a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt index c0b822c13a..38e3ccc69c 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt @@ -237,7 +237,6 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen } companion object { - const val EXTRA_ARGS = "EXTRA_ARGS" const val EXTRA_IMAGE_DATA = "EXTRA_IMAGE_DATA" const val EXTRA_IN_MEMORY_DATA = "EXTRA_IN_MEMORY_DATA" @@ -246,11 +245,11 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmen mediaData: AttachmentData, roomId: String?, eventId: String, - inMemoryData: List?, + inMemoryData: List, sharedTransitionName: String?) = Intent(context, VectorAttachmentViewerActivity::class.java).also { it.putExtra(EXTRA_ARGS, Args(roomId, eventId, sharedTransitionName)) it.putExtra(EXTRA_IMAGE_DATA, mediaData) - if (inMemoryData != null) { + if (inMemoryData.isNotEmpty()) { it.putParcelableArrayListExtra(EXTRA_IN_MEMORY_DATA, ArrayList(inMemoryData)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 6acd041ecf..99eb69f539 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -19,7 +19,6 @@ package im.vector.riotx.features.navigation import android.app.Activity import android.content.Context import android.content.Intent -import android.os.Build import android.view.View import android.view.Window import androidx.core.app.ActivityOptionsCompat @@ -52,7 +51,6 @@ import im.vector.riotx.features.invite.InviteUsersToRoomActivity import im.vector.riotx.features.media.AttachmentData import im.vector.riotx.features.media.BigImageViewerActivity import im.vector.riotx.features.media.VectorAttachmentViewerActivity -import im.vector.riotx.features.media.VideoContentRenderer import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity @@ -245,11 +243,11 @@ class DefaultNavigator @Inject constructor( context.startActivity(WidgetActivity.newIntent(context, widgetArgs)) } - override fun openImageViewer(activity: Activity, + override fun openMediaViewer(activity: Activity, roomId: String, mediaData: AttachmentData, view: View, - inMemory: List?, + inMemory: List, options: ((MutableList>) -> Unit)?) { VectorAttachmentViewerActivity.newIntent(activity, mediaData, @@ -268,52 +266,6 @@ class DefaultNavigator @Inject constructor( pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: "")) options?.invoke(pairs) - val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle() - activity.startActivity(intent, bundle) - } -// val intent = ImageMediaViewerActivity.newIntent(activity, mediaData, ViewCompat.getTransitionName(view)) -// val pairs = ArrayList>() -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { -// activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let { -// pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) -// } -// activity.window.decorView.findViewById(android.R.id.navigationBarBackground)?.let { -// pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) -// } -// } -// pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: "")) -// options?.invoke(pairs) -// -// val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle() -// activity.startActivity(intent, bundle) - } - - override fun openVideoViewer(activity: Activity, - roomId: String, - mediaData: AttachmentData, - view: View, - inMemory: List?, - options: ((MutableList>) -> Unit)?) { -// val intent = VideoMediaViewerActivity.newIntent(activity, mediaData) -// activity.startActivity(intent) - VectorAttachmentViewerActivity.newIntent(activity, - mediaData, - roomId, - mediaData.eventId, - inMemory, - ViewCompat.getTransitionName(view)).let { intent -> - val pairs = ArrayList>() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let { - pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) - } - activity.window.decorView.findViewById(android.R.id.navigationBarBackground)?.let { - pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) - } - } - pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: "")) - options?.invoke(pairs) - val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle() activity.startActivity(intent, bundle) } diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index 6d036f1468..273734916d 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -91,17 +91,10 @@ interface Navigator { fun openRoomWidget(context: Context, roomId: String, widget: Widget) - fun openImageViewer(activity: Activity, + fun openMediaViewer(activity: Activity, roomId: String, mediaData: AttachmentData, view: View, - inMemory: List? = null, - options: ((MutableList>) -> Unit)?) - - fun openVideoViewer(activity: Activity, - roomId: String, - mediaData: VideoContentRenderer.Data, - view: View, - inMemory: List? = null, + inMemory: List = emptyList(), options: ((MutableList>) -> Unit)?) } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt index e0758c7d72..dda070bf48 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt @@ -89,7 +89,7 @@ class RoomUploadsMediaFragment @Inject constructor( // It's very strange i can't just access // the app bar using find by id... - private fun trickFindAppBar() : AppBarLayout? { + private fun trickFindAppBar(): AppBarLayout? { return activity?.supportFragmentManager?.fragments ?.filterIsInstance() ?.firstOrNull() @@ -97,9 +97,14 @@ class RoomUploadsMediaFragment @Inject constructor( } override fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) = withState(uploadsViewModel) { state -> - val inMemory = getItemsArgs(state) - navigator.openImageViewer(requireActivity(), state.roomId, mediaData, view, inMemory) { pairs -> + navigator.openMediaViewer( + activity = requireActivity(), + roomId = state.roomId, + mediaData = mediaData, + view = view, + inMemory = inMemory + ) { pairs -> trickFindAppBar()?.let { pairs.add(Pair(it, ViewCompat.getTransitionName(it) ?: "")) } @@ -151,7 +156,13 @@ class RoomUploadsMediaFragment @Inject constructor( override fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) = withState(uploadsViewModel) { state -> val inMemory = getItemsArgs(state) - navigator.openVideoViewer(requireActivity(), state.roomId, mediaData, view, inMemory) { pairs -> + navigator.openMediaViewer( + activity = requireActivity(), + roomId = state.roomId, + mediaData = mediaData, + view = view, + inMemory = inMemory + ) { pairs -> trickFindAppBar()?.let { pairs.add(Pair(it, ViewCompat.getTransitionName(it) ?: "")) } From ea3e467dc44b042f56a7482db3468d1019b68379 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 10 Jul 2020 12:52:54 +0200 Subject: [PATCH 26/41] Format --- settings.gradle | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/settings.gradle b/settings.gradle index 3a7aa9ac1c..76a15a206d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,6 @@ -include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch', ':attachment-viewer' -include ':multipicker' \ No newline at end of file +include ':vector' +include ':matrix-sdk-android' +include ':matrix-sdk-android-rx' +include ':diff-match-patch' +include ':attachment-viewer' +include ':multipicker' From 6c0f775c4b1058d589eabfd1dc0ea2444999ba7e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 10 Jul 2020 13:07:14 +0200 Subject: [PATCH 27/41] Cleanup --- attachment-viewer/src/main/AndroidManifest.xml | 11 +---------- attachment-viewer/src/main/res/values/dimens.xml | 3 --- attachment-viewer/src/main/res/values/strings.xml | 11 ----------- attachment-viewer/src/main/res/values/styles.xml | 12 ------------ 4 files changed, 1 insertion(+), 36 deletions(-) delete mode 100644 attachment-viewer/src/main/res/values/dimens.xml delete mode 100644 attachment-viewer/src/main/res/values/strings.xml delete mode 100644 attachment-viewer/src/main/res/values/styles.xml diff --git a/attachment-viewer/src/main/AndroidManifest.xml b/attachment-viewer/src/main/AndroidManifest.xml index 4c48526635..ff8ec394d2 100644 --- a/attachment-viewer/src/main/AndroidManifest.xml +++ b/attachment-viewer/src/main/AndroidManifest.xml @@ -1,11 +1,2 @@ - - - - - - - \ No newline at end of file + diff --git a/attachment-viewer/src/main/res/values/dimens.xml b/attachment-viewer/src/main/res/values/dimens.xml deleted file mode 100644 index 125df87119..0000000000 --- a/attachment-viewer/src/main/res/values/dimens.xml +++ /dev/null @@ -1,3 +0,0 @@ - - 16dp - \ No newline at end of file diff --git a/attachment-viewer/src/main/res/values/strings.xml b/attachment-viewer/src/main/res/values/strings.xml deleted file mode 100644 index 6dcb56555a..0000000000 --- a/attachment-viewer/src/main/res/values/strings.xml +++ /dev/null @@ -1,11 +0,0 @@ - - AttachementViewerActivity - - First Fragment - Second Fragment - Next - Previous - - Hello first fragment - Hello second fragment. Arg: %1$s - \ No newline at end of file diff --git a/attachment-viewer/src/main/res/values/styles.xml b/attachment-viewer/src/main/res/values/styles.xml deleted file mode 100644 index a81174782e..0000000000 --- a/attachment-viewer/src/main/res/values/styles.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - \ No newline at end of file From e8b1e418fa116ebe0a9464d1d6f55b3ec70b6e93 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 10 Jul 2020 14:37:57 +0200 Subject: [PATCH 28/41] ktlint --- .../main/java/im/vector/riotx/features/navigation/Navigator.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index 273734916d..2403cfa0a9 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -23,11 +23,10 @@ import androidx.core.util.Pair import androidx.fragment.app.Fragment import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.terms.TermsService -import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.session.widgets.model.Widget +import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes import im.vector.riotx.features.media.AttachmentData -import im.vector.riotx.features.media.VideoContentRenderer import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.share.SharedData import im.vector.riotx.features.terms.ReviewTermsActivity From 9f2631110eb84c824ba6ae91ad670ac3f6c82764 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 10 Jul 2020 14:38:23 +0200 Subject: [PATCH 29/41] Missing copyrights --- .../vector/riotx/attachmentviewer/AttachmentViewerActivity.kt | 1 + .../java/im/vector/riotx/attachmentviewer/SwipeDirection.kt | 1 + .../im/vector/riotx/attachmentviewer/SwipeDirectionDetector.kt | 1 + .../im/vector/riotx/attachmentviewer/SwipeToDismissHandler.kt | 1 + vector/src/main/assets/open_source_licenses.html | 3 +++ 5 files changed, 7 insertions(+) diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt index d6cf7c606a..8c2d4e9833 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/AttachmentViewerActivity.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2020 New Vector Ltd + * Copyright (C) 2018 stfalcon.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeDirection.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeDirection.kt index e552d55efb..ebe8784e15 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeDirection.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeDirection.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2020 New Vector Ltd + * Copyright (C) 2018 stfalcon.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeDirectionDetector.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeDirectionDetector.kt index cedbcd0180..0cf9a19ab1 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeDirectionDetector.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeDirectionDetector.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2020 New Vector Ltd + * Copyright (C) 2018 stfalcon.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeToDismissHandler.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeToDismissHandler.kt index e52c72cba0..ca93d4f73a 100644 --- a/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeToDismissHandler.kt +++ b/attachment-viewer/src/main/java/im/vector/riotx/attachmentviewer/SwipeToDismissHandler.kt @@ -1,5 +1,6 @@ /* * Copyright (c) 2020 New Vector Ltd + * Copyright (C) 2018 stfalcon.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index 3af564aaca..58d63bf5a7 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -389,6 +389,9 @@ SOFTWARE.
  • BillCarsonFr/JsonViewer
  • +
  • + Copyright (C) 2018 stfalcon.com +
  •  Apache License
    
    From 1b6b71ed986a793314ecf9ccb063a69042ffaede Mon Sep 17 00:00:00 2001
    From: Valere 
    Date: Fri, 10 Jul 2020 14:38:31 +0200
    Subject: [PATCH 30/41] Debounce clicks
    
    ---
     .../features/roomprofile/uploads/media/UploadsImageItem.kt | 7 ++++++-
     .../features/roomprofile/uploads/media/UploadsVideoItem.kt | 7 ++++++-
     2 files changed, 12 insertions(+), 2 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt
    index f994ad0110..3b83e99656 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt
    @@ -24,6 +24,7 @@ import com.airbnb.epoxy.EpoxyModelClass
     import im.vector.riotx.R
     import im.vector.riotx.core.epoxy.VectorEpoxyHolder
     import im.vector.riotx.core.epoxy.VectorEpoxyModel
    +import im.vector.riotx.core.utils.DebouncedClickListener
     import im.vector.riotx.features.media.ImageContentRenderer
     
     @EpoxyModelClass(layout = R.layout.item_uploads_image)
    @@ -36,7 +37,11 @@ abstract class UploadsImageItem : VectorEpoxyModel() {
     
         override fun bind(holder: Holder) {
             super.bind(holder)
    -        holder.view.setOnClickListener { listener?.onItemClicked(holder.imageView, data) }
    +        holder.view.setOnClickListener(
    +                DebouncedClickListener(View.OnClickListener { _ ->
    +                    listener?.onItemClicked(holder.imageView, data)
    +                })
    +        )
             imageContentRenderer.render(data, holder.imageView, IMAGE_SIZE_DP)
             ViewCompat.setTransitionName(holder.imageView, "imagePreview_${id()}")
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt
    index 1c9ab4ae74..f20f6ed5b1 100644
    --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt
    @@ -24,6 +24,7 @@ import com.airbnb.epoxy.EpoxyModelClass
     import im.vector.riotx.R
     import im.vector.riotx.core.epoxy.VectorEpoxyHolder
     import im.vector.riotx.core.epoxy.VectorEpoxyModel
    +import im.vector.riotx.core.utils.DebouncedClickListener
     import im.vector.riotx.features.media.ImageContentRenderer
     import im.vector.riotx.features.media.VideoContentRenderer
     
    @@ -37,7 +38,11 @@ abstract class UploadsVideoItem : VectorEpoxyModel() {
     
         override fun bind(holder: Holder) {
             super.bind(holder)
    -        holder.view.setOnClickListener { listener?.onItemClicked(holder.imageView, data) }
    +        holder.view.setOnClickListener(
    +            DebouncedClickListener(View.OnClickListener { _ ->
    +                listener?.onItemClicked(holder.imageView, data)
    +            })
    +        )
             imageContentRenderer.render(data.thumbnailMediaData, holder.imageView, IMAGE_SIZE_DP)
             ViewCompat.setTransitionName(holder.imageView, "videoPreview_${id()}")
         }
    
    From 08bc487f170ca1de284d1d601e262d28660df12c Mon Sep 17 00:00:00 2001
    From: Valere 
    Date: Fri, 10 Jul 2020 14:39:08 +0200
    Subject: [PATCH 31/41] klint
    
    ---
     .../main/java/im/vector/riotx/features/navigation/Navigator.kt   | 1 -
     1 file changed, 1 deletion(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
    index 273734916d..c86aa0aca9 100644
    --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
    @@ -27,7 +27,6 @@ import im.vector.matrix.android.api.util.MatrixItem
     import im.vector.matrix.android.api.session.widgets.model.Widget
     import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes
     import im.vector.riotx.features.media.AttachmentData
    -import im.vector.riotx.features.media.VideoContentRenderer
     import im.vector.riotx.features.settings.VectorSettingsActivity
     import im.vector.riotx.features.share.SharedData
     import im.vector.riotx.features.terms.ReviewTermsActivity
    
    From 10f8aebde25b190ca2b74e50939c76cb0109fb73 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 3 Jul 2020 11:14:41 +0200
    Subject: [PATCH 32/41] Update comment
    
    ---
     .../java/im/vector/matrix/android/api/MatrixConfiguration.kt    | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixConfiguration.kt
    index e7c24fadc8..d80a940675 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixConfiguration.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixConfiguration.kt
    @@ -33,7 +33,7 @@ data class MatrixConfiguration(
             ),
             /**
              * Optional proxy to connect to the matrix servers
    -         * You can create one using for instance Proxy(proxyType, InetSocketAddress(hostname, port)
    +         * You can create one using for instance Proxy(proxyType, InetSocketAddress.createUnresolved(hostname, port)
              */
             val proxy: Proxy? = null
     ) {
    
    From 5f60d7fd3bf9ca683fe7875516763d72f0e18c44 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 3 Jul 2020 11:16:58 +0200
    Subject: [PATCH 33/41] Session.configureAndStart now handle registering to
     webRtcPeerConnectionManager...
    
    ---
     vector/src/main/java/im/vector/riotx/VectorApplication.kt | 8 ++++++--
     .../main/java/im/vector/riotx/core/extensions/Session.kt  | 3 +++
     .../java/im/vector/riotx/features/login/LoginViewModel.kt | 7 +++++--
     3 files changed, 14 insertions(+), 4 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    index ab7c3e1bf7..d49c7a4c7b 100644
    --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    @@ -137,8 +137,12 @@ class VectorApplication :
             if (authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
                 val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
                 activeSessionHolder.setActiveSession(lastAuthenticatedSession)
    -            lastAuthenticatedSession.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener)
    -            lastAuthenticatedSession.callSignalingService().addCallListener(webRtcPeerConnectionManager)
    +            lastAuthenticatedSession.configureAndStart(
    +                    applicationContext,
    +                    pushRuleTriggerListener,
    +                    webRtcPeerConnectionManager,
    +                    sessionListener
    +            )
             }
             ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
                 @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
    index 29b169ffd4..788ab01c4d 100644
    --- a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
    @@ -24,12 +24,14 @@ import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
     import im.vector.matrix.android.api.session.sync.FilterService
     import im.vector.riotx.core.services.VectorSyncService
    +import im.vector.riotx.features.call.WebRtcPeerConnectionManager
     import im.vector.riotx.features.notifications.PushRuleTriggerListener
     import im.vector.riotx.features.session.SessionListener
     import timber.log.Timber
     
     fun Session.configureAndStart(context: Context,
                                   pushRuleTriggerListener: PushRuleTriggerListener,
    +                              webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
                                   sessionListener: SessionListener) {
         open()
         addListener(sessionListener)
    @@ -38,6 +40,7 @@ fun Session.configureAndStart(context: Context,
         startSyncing(context)
         refreshPushers()
         pushRuleTriggerListener.startWithSession(this)
    +    callSignalingService().addCallListener(webRtcPeerConnectionManager)
     }
     
     fun Session.startSyncing(context: Context) {
    diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt
    index 7edc674b11..f85d91d9c5 100644
    --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt
    @@ -667,8 +667,11 @@ class LoginViewModel @AssistedInject constructor(
     
         private fun onSessionCreated(session: Session) {
             activeSessionHolder.setActiveSession(session)
    -        session.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener)
    -        session.callSignalingService().addCallListener(webRtcPeerConnectionManager)
    +        session.configureAndStart(
    +                applicationContext,
    +                pushRuleTriggerListener,
    +                webRtcPeerConnectionManager,
    +                sessionListener)
             setState {
                 copy(
                         asyncLoginAction = Success(Unit)
    
    From 6569ee5d109189513d3c19b1d5e8a57a1adbd332 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 3 Jul 2020 15:36:04 +0200
    Subject: [PATCH 34/41] Use Set instead of List
    
    ---
     .../vector/matrix/android/internal/session/SessionListeners.kt  | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionListeners.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionListeners.kt
    index ff3bc0b073..83b90b16b9 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionListeners.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionListeners.kt
    @@ -22,7 +22,7 @@ import javax.inject.Inject
     
     internal class SessionListeners @Inject constructor() {
     
    -    private val listeners = ArrayList()
    +    private val listeners = mutableSetOf()
     
         fun addListener(listener: Session.Listener) {
             synchronized(listeners) {
    
    From 811cbb2e201318f1b964ac136e2e1fce05e33b05 Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 3 Jul 2020 15:42:31 +0200
    Subject: [PATCH 35/41] ActiveSessionHolder to more things related to other
     @Singleton, and especially some missing cleanup Such as calling
     `removeListener()` and `callSignalingService().removeCallListener()`
     `Session.configureAndStart()` do less thing now
    
    ---
     .../java/im/vector/riotx/VectorApplication.kt | 13 +----
     .../riotx/core/di/ActiveSessionHolder.kt      | 23 ++++++++-
     .../vector/riotx/core/extensions/Session.kt   | 13 +----
     .../im/vector/riotx/core/utils/DataSource.kt  |  3 ++
     .../call/WebRtcPeerConnectionManager.kt       | 51 ++++++++++---------
     .../timeline/format/NoticeEventFormatter.kt   | 12 +++--
     .../riotx/features/login/LoginViewModel.kt    | 16 ++----
     .../NotificationDrawerManager.kt              | 15 ++++--
     .../notifications/OutdatedEventDetector.kt    | 10 ++--
     .../notifications/PushRuleTriggerListener.kt  |  8 +--
     10 files changed, 87 insertions(+), 77 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    index d49c7a4c7b..d7fe2a054d 100644
    --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    @@ -44,15 +44,12 @@ import im.vector.riotx.core.di.HasVectorInjector
     import im.vector.riotx.core.di.VectorComponent
     import im.vector.riotx.core.extensions.configureAndStart
     import im.vector.riotx.core.rx.RxConfig
    -import im.vector.riotx.features.call.WebRtcPeerConnectionManager
     import im.vector.riotx.features.configuration.VectorConfiguration
     import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks
     import im.vector.riotx.features.notifications.NotificationDrawerManager
     import im.vector.riotx.features.notifications.NotificationUtils
    -import im.vector.riotx.features.notifications.PushRuleTriggerListener
     import im.vector.riotx.features.popup.PopupAlertManager
     import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
    -import im.vector.riotx.features.session.SessionListener
     import im.vector.riotx.features.settings.VectorPreferences
     import im.vector.riotx.features.version.VersionProvider
     import im.vector.riotx.push.fcm.FcmHelper
    @@ -79,16 +76,13 @@ class VectorApplication :
         @Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper
         @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
         @Inject lateinit var activeSessionHolder: ActiveSessionHolder
    -    @Inject lateinit var sessionListener: SessionListener
         @Inject lateinit var notificationDrawerManager: NotificationDrawerManager
    -    @Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener
         @Inject lateinit var vectorPreferences: VectorPreferences
         @Inject lateinit var versionProvider: VersionProvider
         @Inject lateinit var notificationUtils: NotificationUtils
         @Inject lateinit var appStateHandler: AppStateHandler
         @Inject lateinit var rxConfig: RxConfig
         @Inject lateinit var popupAlertManager: PopupAlertManager
    -    @Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
     
         lateinit var vectorComponent: VectorComponent
     
    @@ -137,12 +131,7 @@ class VectorApplication :
             if (authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
                 val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
                 activeSessionHolder.setActiveSession(lastAuthenticatedSession)
    -            lastAuthenticatedSession.configureAndStart(
    -                    applicationContext,
    -                    pushRuleTriggerListener,
    -                    webRtcPeerConnectionManager,
    -                    sessionListener
    -            )
    +            lastAuthenticatedSession.configureAndStart(applicationContext)
             }
             ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
                 @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
    index ff9865c3ea..9fbd51984f 100644
    --- a/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
    @@ -20,8 +20,12 @@ import arrow.core.Option
     import im.vector.matrix.android.api.auth.AuthenticationService
     import im.vector.matrix.android.api.session.Session
     import im.vector.riotx.ActiveSessionDataSource
    +import im.vector.riotx.features.call.WebRtcPeerConnectionManager
     import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
     import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler
    +import im.vector.riotx.features.notifications.PushRuleTriggerListener
    +import im.vector.riotx.features.session.SessionListener
    +import timber.log.Timber
     import java.util.concurrent.atomic.AtomicReference
     import javax.inject.Inject
     import javax.inject.Singleton
    @@ -30,23 +34,40 @@ import javax.inject.Singleton
     class ActiveSessionHolder @Inject constructor(private val authenticationService: AuthenticationService,
                                                   private val sessionObservableStore: ActiveSessionDataSource,
                                                   private val keyRequestHandler: KeyRequestHandler,
    -                                              private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler
    +                                              private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler,
    +                                              private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
    +                                              private val pushRuleTriggerListener: PushRuleTriggerListener,
    +                                              private val sessionListener: SessionListener
     ) {
     
         private var activeSession: AtomicReference = AtomicReference()
     
         fun setActiveSession(session: Session) {
    +        Timber.w("setActiveSession of ${session.myUserId}")
             activeSession.set(session)
             sessionObservableStore.post(Option.just(session))
    +
             keyRequestHandler.start(session)
             incomingVerificationRequestHandler.start(session)
    +        session.addListener(sessionListener)
    +        pushRuleTriggerListener.startWithSession(session)
    +        session.callSignalingService().addCallListener(webRtcPeerConnectionManager)
         }
     
         fun clearActiveSession() {
    +        // Do some cleanup first
    +        getSafeActiveSession()?.let {
    +            Timber.w("clearActiveSession of ${it.myUserId}")
    +            it.callSignalingService().removeCallListener(webRtcPeerConnectionManager)
    +            it.removeListener(sessionListener)
    +        }
    +
             activeSession.set(null)
             sessionObservableStore.post(Option.empty())
    +
             keyRequestHandler.stop()
             incomingVerificationRequestHandler.stop()
    +        pushRuleTriggerListener.stop()
         }
     
         fun hasActiveSession(): Boolean {
    diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
    index 788ab01c4d..d212380da4 100644
    --- a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
    @@ -24,23 +24,14 @@ import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
     import im.vector.matrix.android.api.session.sync.FilterService
     import im.vector.riotx.core.services.VectorSyncService
    -import im.vector.riotx.features.call.WebRtcPeerConnectionManager
    -import im.vector.riotx.features.notifications.PushRuleTriggerListener
    -import im.vector.riotx.features.session.SessionListener
     import timber.log.Timber
     
    -fun Session.configureAndStart(context: Context,
    -                              pushRuleTriggerListener: PushRuleTriggerListener,
    -                              webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
    -                              sessionListener: SessionListener) {
    +fun Session.configureAndStart(context: Context) {
    +    Timber.i("Configure and start session for $myUserId")
         open()
    -    addListener(sessionListener)
         setFilter(FilterService.FilterPreset.RiotFilter)
    -    Timber.i("Configure and start session for ${this.myUserId}")
         startSyncing(context)
         refreshPushers()
    -    pushRuleTriggerListener.startWithSession(this)
    -    callSignalingService().addCallListener(webRtcPeerConnectionManager)
     }
     
     fun Session.startSyncing(context: Context) {
    diff --git a/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt b/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt
    index 4c4a553e5c..6f6057cb43 100644
    --- a/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt
    @@ -36,6 +36,9 @@ open class BehaviorDataSource(private val defaultValue: T? = null) : MutableD
     
         private val behaviorRelay = createRelay()
     
    +    val currentValue: T?
    +        get() = behaviorRelay.value
    +
         override fun observe(): Observable {
             return behaviorRelay.hide().observeOn(AndroidSchedulers.mainThread())
         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt b/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt
    index 05f14ae4f2..070375d201 100644
    --- a/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/call/WebRtcPeerConnectionManager.kt
    @@ -22,6 +22,7 @@ import android.os.Build
     import androidx.annotation.RequiresApi
     import im.vector.matrix.android.api.MatrixCallback
     import im.vector.matrix.android.api.extensions.tryThis
    +import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.call.CallState
     import im.vector.matrix.android.api.session.call.CallsListener
     import im.vector.matrix.android.api.session.call.EglUtils
    @@ -31,7 +32,7 @@ import im.vector.matrix.android.api.session.room.model.call.CallAnswerContent
     import im.vector.matrix.android.api.session.room.model.call.CallCandidatesContent
     import im.vector.matrix.android.api.session.room.model.call.CallHangupContent
     import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
    -import im.vector.riotx.core.di.ActiveSessionHolder
    +import im.vector.riotx.ActiveSessionDataSource
     import im.vector.riotx.core.services.BluetoothHeadsetReceiver
     import im.vector.riotx.core.services.CallService
     import im.vector.riotx.core.services.WiredHeadsetStateReceiver
    @@ -71,9 +72,12 @@ import javax.inject.Singleton
     @Singleton
     class WebRtcPeerConnectionManager @Inject constructor(
             private val context: Context,
    -        private val sessionHolder: ActiveSessionHolder
    +        private val activeSessionDataSource: ActiveSessionDataSource
     ) : CallsListener {
     
    +    private val currentSession: Session?
    +        get() = activeSessionDataSource.currentValue?.orNull()
    +
         interface CurrentCallListener {
             fun onCurrentCallChange(call: MxCall?)
             fun onCaptureStateChanged(mgr: WebRtcPeerConnectionManager) {}
    @@ -288,15 +292,16 @@ class WebRtcPeerConnectionManager @Inject constructor(
         }
     
         private fun getTurnServer(callback: ((TurnServerResponse?) -> Unit)) {
    -        sessionHolder.getActiveSession().callSignalingService().getTurnServer(object : MatrixCallback {
    -            override fun onSuccess(data: TurnServerResponse?) {
    -                callback(data)
    -            }
    +        currentSession?.callSignalingService()
    +                ?.getTurnServer(object : MatrixCallback {
    +                    override fun onSuccess(data: TurnServerResponse?) {
    +                        callback(data)
    +                    }
     
    -            override fun onFailure(failure: Throwable) {
    -                callback(null)
    -            }
    -        })
    +                    override fun onFailure(failure: Throwable) {
    +                        callback(null)
    +                    }
    +                })
         }
     
         fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) {
    @@ -310,7 +315,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
             currentCall?.mxCall
                     ?.takeIf { it.state is CallState.Connected }
                     ?.let { mxCall ->
    -                    val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName()
    +                    val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
                                 ?: mxCall.roomId
                         // Start background service with notification
                         CallService.onPendingCall(
    @@ -318,7 +323,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
                                 isVideo = mxCall.isVideoCall,
                                 roomName = name,
                                 roomId = mxCall.roomId,
    -                            matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
    +                            matrixId = currentSession?.myUserId ?: "",
                                 callId = mxCall.callId)
                     }
     
    @@ -373,14 +378,14 @@ class WebRtcPeerConnectionManager @Inject constructor(
             val mxCall = callContext.mxCall
             // Update service state
     
    -        val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName()
    +        val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
                     ?: mxCall.roomId
             CallService.onPendingCall(
                     context = context,
                     isVideo = mxCall.isVideoCall,
                     roomName = name,
                     roomId = mxCall.roomId,
    -                matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
    +                matrixId = currentSession?.myUserId ?: "",
                     callId = mxCall.callId
             )
             executor.execute {
    @@ -563,14 +568,14 @@ class WebRtcPeerConnectionManager @Inject constructor(
                         ?.let { mxCall ->
                             // Start background service with notification
     
    -                        val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName()
    +                        val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
                                     ?: mxCall.otherUserId
                             CallService.onOnGoingCallBackground(
                                     context = context,
                                     isVideo = mxCall.isVideoCall,
                                     roomName = name,
                                     roomId = mxCall.roomId,
    -                                matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
    +                                matrixId = currentSession?.myUserId ?: "",
                                     callId = mxCall.callId
                             )
                         }
    @@ -631,20 +636,20 @@ class WebRtcPeerConnectionManager @Inject constructor(
             }
     
             Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall")
    -        val createdCall = sessionHolder.getSafeActiveSession()?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return
    +        val createdCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return
             val callContext = CallContext(createdCall)
     
             audioManager.startForCall(createdCall)
             currentCall = callContext
     
    -        val name = sessionHolder.getSafeActiveSession()?.getUser(createdCall.otherUserId)?.getBestName()
    +        val name = currentSession?.getUser(createdCall.otherUserId)?.getBestName()
                     ?: createdCall.otherUserId
             CallService.onOutgoingCallRinging(
                     context = context.applicationContext,
                     isVideo = createdCall.isVideoCall,
                     roomName = name,
                     roomId = createdCall.roomId,
    -                matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
    +                matrixId = currentSession?.myUserId ?: "",
                     callId = createdCall.callId)
     
             executor.execute {
    @@ -693,14 +698,14 @@ class WebRtcPeerConnectionManager @Inject constructor(
             }
     
             // Start background service with notification
    -        val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName()
    +        val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
                     ?: mxCall.otherUserId
             CallService.onIncomingCallRinging(
                     context = context,
                     isVideo = mxCall.isVideoCall,
                     roomName = name,
                     roomId = mxCall.roomId,
    -                matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
    +                matrixId = currentSession?.myUserId ?: "",
                     callId = mxCall.callId
             )
     
    @@ -818,14 +823,14 @@ class WebRtcPeerConnectionManager @Inject constructor(
             }
             val mxCall = call.mxCall
             // Update service state
    -        val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName()
    +        val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
                     ?: mxCall.otherUserId
             CallService.onPendingCall(
                     context = context,
                     isVideo = mxCall.isVideoCall,
                     roomName = name,
                     roomId = mxCall.roomId,
    -                matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "",
    +                matrixId = currentSession?.myUserId ?: "",
                     callId = mxCall.callId
             )
             executor.execute {
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
    index c1f4187e0b..655621f9ad 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
    @@ -40,17 +40,20 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
     import im.vector.matrix.android.api.session.widgets.model.WidgetContent
     import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
     import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
    +import im.vector.riotx.ActiveSessionDataSource
     import im.vector.riotx.R
    -import im.vector.riotx.core.di.ActiveSessionHolder
     import im.vector.riotx.core.resources.StringProvider
     import timber.log.Timber
     import javax.inject.Inject
     
    -class NoticeEventFormatter @Inject constructor(private val sessionHolder: ActiveSessionHolder,
    +class NoticeEventFormatter @Inject constructor(private val activeSessionDataSource: ActiveSessionDataSource,
                                                    private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
                                                    private val sp: StringProvider) {
     
    -    private fun Event.isSentByCurrentUser() = senderId != null && senderId == sessionHolder.getSafeActiveSession()?.myUserId
    +    private val currentUserId: String?
    +        get() = activeSessionDataSource.currentValue?.orNull()?.myUserId
    +
    +    private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId
     
         fun format(timelineEvent: TimelineEvent): CharSequence? {
             return when (val type = timelineEvent.root.getClearType()) {
    @@ -449,7 +452,6 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
             val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: event.stateKey ?: ""
             return when (eventContent?.membership) {
                 Membership.INVITE -> {
    -                val selfUserId = sessionHolder.getSafeActiveSession()?.myUserId
                     when {
                         eventContent.thirdPartyInvite != null -> {
                             val userWhoHasAccepted = eventContent.thirdPartyInvite?.signed?.mxid ?: event.stateKey
    @@ -466,7 +468,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
                                 sp.getString(R.string.notice_room_third_party_registered_invite, userWhoHasAccepted, threePidDisplayName)
                             }
                         }
    -                    event.stateKey == selfUserId          ->
    +                    event.stateKey == currentUserId       ->
                             eventContent.safeReason?.let { reason ->
                                 sp.getString(R.string.notice_room_invite_you_with_reason, senderDisplayName, reason)
                             } ?: sp.getString(R.string.notice_room_invite_you, senderDisplayName)
    diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt
    index f85d91d9c5..071e23c252 100644
    --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt
    @@ -49,9 +49,6 @@ import im.vector.riotx.core.extensions.exhaustive
     import im.vector.riotx.core.platform.VectorViewModel
     import im.vector.riotx.core.resources.StringProvider
     import im.vector.riotx.core.utils.ensureTrailingSlash
    -import im.vector.riotx.features.call.WebRtcPeerConnectionManager
    -import im.vector.riotx.features.notifications.PushRuleTriggerListener
    -import im.vector.riotx.features.session.SessionListener
     import im.vector.riotx.features.signout.soft.SoftLogoutActivity
     import timber.log.Timber
     import java.util.concurrent.CancellationException
    @@ -64,13 +61,10 @@ class LoginViewModel @AssistedInject constructor(
             private val applicationContext: Context,
             private val authenticationService: AuthenticationService,
             private val activeSessionHolder: ActiveSessionHolder,
    -        private val pushRuleTriggerListener: PushRuleTriggerListener,
             private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory,
    -        private val sessionListener: SessionListener,
             private val reAuthHelper: ReAuthHelper,
    -        private val stringProvider: StringProvider,
    -        private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager)
    -    : VectorViewModel(initialState) {
    +        private val stringProvider: StringProvider
    +) : VectorViewModel(initialState) {
     
         @AssistedInject.Factory
         interface Factory {
    @@ -667,11 +661,7 @@ class LoginViewModel @AssistedInject constructor(
     
         private fun onSessionCreated(session: Session) {
             activeSessionHolder.setActiveSession(session)
    -        session.configureAndStart(
    -                applicationContext,
    -                pushRuleTriggerListener,
    -                webRtcPeerConnectionManager,
    -                sessionListener)
    +        session.configureAndStart(applicationContext)
             setState {
                 copy(
                         asyncLoginAction = Success(Unit)
    diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt
    index 6fc396b264..d0839795dd 100644
    --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt
    @@ -22,10 +22,11 @@ import android.os.HandlerThread
     import androidx.annotation.WorkerThread
     import androidx.core.app.NotificationCompat
     import androidx.core.app.Person
    +import im.vector.matrix.android.api.session.Session
     import im.vector.matrix.android.api.session.content.ContentUrlResolver
    +import im.vector.riotx.ActiveSessionDataSource
     import im.vector.riotx.BuildConfig
     import im.vector.riotx.R
    -import im.vector.riotx.core.di.ActiveSessionHolder
     import im.vector.riotx.core.resources.StringProvider
     import im.vector.riotx.features.settings.VectorPreferences
     import me.gujun.android.span.span
    @@ -46,7 +47,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
                                                         private val notificationUtils: NotificationUtils,
                                                         private val vectorPreferences: VectorPreferences,
                                                         private val stringProvider: StringProvider,
    -                                                    private val activeSessionHolder: ActiveSessionHolder,
    +                                                    private val activeSessionDataSource: ActiveSessionDataSource,
                                                         private val iconLoader: IconLoader,
                                                         private val bitmapLoader: BitmapLoader,
                                                         private val outdatedDetector: OutdatedEventDetector?) {
    @@ -68,6 +69,10 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
     
         private var currentRoomId: String? = null
     
    +    // TODO Multi-session: this will have to be improved
    +    private val currentSession: Session?
    +        get() = activeSessionDataSource.currentValue?.orNull()
    +
         /**
         Should be called as soon as a new event is ready to be displayed.
         The notification corresponding to this event will not be displayed until
    @@ -204,7 +209,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
         private fun refreshNotificationDrawerBg() {
             Timber.v("refreshNotificationDrawerBg()")
     
    -        val session = activeSessionHolder.getSafeActiveSession() ?: return
    +        val session = currentSession ?: return
     
             val user = session.getUser(session.myUserId)
             // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash
    @@ -474,7 +479,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
                     val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
                     if (!file.exists()) file.createNewFile()
                     FileOutputStream(file).use {
    -                    activeSessionHolder.getSafeActiveSession()?.securelyStoreObject(eventList, KEY_ALIAS_SECRET_STORAGE, it)
    +                    currentSession?.securelyStoreObject(eventList, KEY_ALIAS_SECRET_STORAGE, it)
                     }
                 } catch (e: Throwable) {
                     Timber.e(e, "## Failed to save cached notification info")
    @@ -487,7 +492,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
                 val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
                 if (file.exists()) {
                     FileInputStream(file).use {
    -                    val events: ArrayList? = activeSessionHolder.getSafeActiveSession()?.loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE)
    +                    val events: ArrayList? = currentSession?.loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE)
                         if (events != null) {
                             return events.toMutableList()
                         }
    diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/OutdatedEventDetector.kt b/vector/src/main/java/im/vector/riotx/features/notifications/OutdatedEventDetector.kt
    index 6b8d3dae49..d2b939bc99 100644
    --- a/vector/src/main/java/im/vector/riotx/features/notifications/OutdatedEventDetector.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/notifications/OutdatedEventDetector.kt
    @@ -15,10 +15,12 @@
      */
     package im.vector.riotx.features.notifications
     
    -import im.vector.riotx.core.di.ActiveSessionHolder
    +import im.vector.riotx.ActiveSessionDataSource
     import javax.inject.Inject
     
    -class OutdatedEventDetector @Inject constructor(private val activeSessionHolder: ActiveSessionHolder) {
    +class OutdatedEventDetector @Inject constructor(
    +        private val activeSessionDataSource: ActiveSessionDataSource
    +) {
     
         /**
          * Returns true if the given event is outdated.
    @@ -26,10 +28,12 @@ class OutdatedEventDetector @Inject constructor(private val activeSessionHolder:
          * other device.
          */
         fun isMessageOutdated(notifiableEvent: NotifiableEvent): Boolean {
    +        val session = activeSessionDataSource.currentValue?.orNull() ?: return false
    +
             if (notifiableEvent is NotifiableMessageEvent) {
                 val eventID = notifiableEvent.eventId
                 val roomID = notifiableEvent.roomId
    -            val room = activeSessionHolder.getSafeActiveSession()?.getRoom(roomID) ?: return false
    +            val room = session.getRoom(roomID) ?: return false
                 return room.isEventRead(eventID)
             }
             return false
    diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt b/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt
    index 4ba89c02e2..adef246151 100644
    --- a/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/notifications/PushRuleTriggerListener.kt
    @@ -30,17 +30,17 @@ class PushRuleTriggerListener @Inject constructor(
             private val notificationDrawerManager: NotificationDrawerManager
     ) : PushRuleService.PushRuleListener {
     
    -    var session: Session? = null
    +    private var session: Session? = null
     
         override fun onMatchRule(event: Event, actions: List) {
             Timber.v("Push rule match for event ${event.eventId}")
    -        if (session == null) {
    +        val safeSession = session ?: return Unit.also {
                 Timber.e("Called without active session")
    -            return
             }
    +
             val notificationAction = actions.toNotificationAction()
             if (notificationAction.shouldNotify) {
    -            val notifiableEvent = resolver.resolveEvent(event, session!!)
    +            val notifiableEvent = resolver.resolveEvent(event, safeSession)
                 if (notifiableEvent == null) {
                     Timber.v("## Failed to resolve event")
                     // TODO
    
    From 633548f190696507fc3ffdd27c380367c78827fc Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 3 Jul 2020 16:00:39 +0200
    Subject: [PATCH 36/41] Create ImageManager to be able to (re-)configure the
     lib
    
    ---
     .../java/im/vector/riotx/VectorApplication.kt |  3 --
     .../riotx/core/di/ActiveSessionHolder.kt      |  4 ++-
     .../im/vector/riotx/core/di/ImageManager.kt   | 35 +++++++++++++++++++
     3 files changed, 38 insertions(+), 4 deletions(-)
     create mode 100644 vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt
    
    diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    index d7fe2a054d..db14dba93d 100644
    --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt
    @@ -32,8 +32,6 @@ import com.airbnb.epoxy.EpoxyAsyncUtil
     import com.airbnb.epoxy.EpoxyController
     import com.facebook.stetho.Stetho
     import com.gabrielittner.threetenbp.LazyThreeTen
    -import com.github.piasy.biv.BigImageViewer
    -import com.github.piasy.biv.loader.glide.GlideImageLoader
     import im.vector.matrix.android.api.Matrix
     import im.vector.matrix.android.api.MatrixConfiguration
     import im.vector.matrix.android.api.auth.AuthenticationService
    @@ -108,7 +106,6 @@ class VectorApplication :
             logInfo()
             LazyThreeTen.init(this)
     
    -        BigImageViewer.initialize(GlideImageLoader.with(applicationContext))
             EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
             EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
             registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks(popupAlertManager))
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
    index 9fbd51984f..2dc7b24ebf 100644
    --- a/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/di/ActiveSessionHolder.kt
    @@ -37,7 +37,8 @@ class ActiveSessionHolder @Inject constructor(private val authenticationService:
                                                   private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler,
                                                   private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
                                                   private val pushRuleTriggerListener: PushRuleTriggerListener,
    -                                              private val sessionListener: SessionListener
    +                                              private val sessionListener: SessionListener,
    +                                              private val imageManager: ImageManager
     ) {
     
         private var activeSession: AtomicReference = AtomicReference()
    @@ -52,6 +53,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticationService:
             session.addListener(sessionListener)
             pushRuleTriggerListener.startWithSession(session)
             session.callSignalingService().addCallListener(webRtcPeerConnectionManager)
    +        imageManager.onSessionStarted(session)
         }
     
         fun clearActiveSession() {
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt b/vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt
    new file mode 100644
    index 0000000000..7972ebb163
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt
    @@ -0,0 +1,35 @@
    +/*
    + * 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.riotx.core.di
    +
    +import android.content.Context
    +import com.github.piasy.biv.BigImageViewer
    +import com.github.piasy.biv.loader.glide.GlideImageLoader
    +import im.vector.matrix.android.api.session.Session
    +import javax.inject.Inject
    +
    +/**
    + * This class is used to configure the library we use for images
    + */
    +class ImageManager @Inject constructor(
    +        private val context: Context
    +) {
    +
    +    fun onSessionStarted(session: Session) {
    +        BigImageViewer.initialize(GlideImageLoader.with(context))
    +    }
    +}
    
    From eda29e3fefb9388cafd06374011ba54ad29e591a Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 3 Jul 2020 17:27:00 +0200
    Subject: [PATCH 37/41] Add link for WebRTC
    
    ---
     docs/voip_signaling.md | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/docs/voip_signaling.md b/docs/voip_signaling.md
    index c80cdd6b96..e055b4cd35 100644
    --- a/docs/voip_signaling.md
    +++ b/docs/voip_signaling.md
    @@ -1,5 +1,6 @@
     Useful links:
     - https://codelabs.developers.google.com/codelabs/webrtc-web/#0
    +- http://webrtc.github.io/webrtc-org/native-code/android/
     
     
        ╔════════════════════════════════════════════════╗
    
    From f179fc523d8bcbfd4e0006379ed4fc1f5f8291eb Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 3 Jul 2020 18:35:59 +0200
    Subject: [PATCH 38/41] Give configured OkHttpClient to Glide and
     BigImageViewer
    
    ---
     .../matrix/android/api/session/Session.kt     |  8 ++++
     .../internal/session/DefaultSession.kt        | 12 +++++-
     .../im/vector/riotx/core/di/ImageManager.kt   | 16 +++++++-
     .../im/vector/riotx/core/glide/FactoryUrl.kt  | 38 +++++++++++++++++++
     .../core/glide/VectorGlideModelLoader.kt      |  2 +-
     5 files changed, 71 insertions(+), 5 deletions(-)
     create mode 100644 vector/src/main/java/im/vector/riotx/core/glide/FactoryUrl.kt
    
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt
    index 5b0f24aed7..8d97dfc01b 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt
    @@ -47,6 +47,7 @@ import im.vector.matrix.android.api.session.terms.TermsService
     import im.vector.matrix.android.api.session.typing.TypingUsersTracker
     import im.vector.matrix.android.api.session.user.UserService
     import im.vector.matrix.android.api.session.widgets.WidgetService
    +import okhttp3.OkHttpClient
     
     /**
      * This interface defines interactions with a session.
    @@ -205,6 +206,13 @@ interface Session :
          */
         fun removeListener(listener: Listener)
     
    +    /**
    +     * Will return a OkHttpClient which will manage pinned certificates and Proxy if configured.
    +     * It will not add any access-token to the request.
    +     * So it is exposed to let the app be able to download image with Glide or any other libraries which accept an OkHttp client.
    +     */
    +    fun getOkHttpClient(): OkHttpClient
    +
         /**
          * A global session listener to get notified for some events.
          */
    diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt
    index 83ba76d5b8..16179dd64a 100644
    --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt
    +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt
    @@ -52,6 +52,7 @@ import im.vector.matrix.android.internal.auth.SessionParamsStore
     import im.vector.matrix.android.internal.crypto.DefaultCryptoService
     import im.vector.matrix.android.internal.di.SessionDatabase
     import im.vector.matrix.android.internal.di.SessionId
    +import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate
     import im.vector.matrix.android.internal.di.WorkManagerProvider
     import im.vector.matrix.android.internal.session.identity.DefaultIdentityService
     import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecryptor
    @@ -64,6 +65,7 @@ import im.vector.matrix.android.internal.util.createUIHandler
     import io.realm.RealmConfiguration
     import kotlinx.coroutines.Dispatchers
     import kotlinx.coroutines.launch
    +import okhttp3.OkHttpClient
     import org.greenrobot.eventbus.EventBus
     import org.greenrobot.eventbus.Subscribe
     import org.greenrobot.eventbus.ThreadMode
    @@ -113,8 +115,10 @@ internal class DefaultSession @Inject constructor(
             private val defaultIdentityService: DefaultIdentityService,
             private val integrationManagerService: IntegrationManagerService,
             private val taskExecutor: TaskExecutor,
    -        private val callSignalingService: Lazy)
    -    : Session,
    +        private val callSignalingService: Lazy,
    +        @UnauthenticatedWithCertificate
    +        private val unauthenticatedWithCertificateOkHttpClient: Lazy
    +) : Session,
             RoomService by roomService.get(),
             RoomDirectoryService by roomDirectoryService.get(),
             GroupService by groupService.get(),
    @@ -255,6 +259,10 @@ internal class DefaultSession @Inject constructor(
     
         override fun callSignalingService(): CallSignalingService = callSignalingService.get()
     
    +    override fun getOkHttpClient(): OkHttpClient {
    +        return unauthenticatedWithCertificateOkHttpClient.get()
    +    }
    +
         override fun addListener(listener: Session.Listener) {
             sessionListeners.addListener(listener)
         }
    diff --git a/vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt b/vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt
    index 7972ebb163..74a01e76ec 100644
    --- a/vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt
    @@ -17,19 +17,31 @@
     package im.vector.riotx.core.di
     
     import android.content.Context
    +import com.bumptech.glide.Glide
    +import com.bumptech.glide.load.model.GlideUrl
     import com.github.piasy.biv.BigImageViewer
     import com.github.piasy.biv.loader.glide.GlideImageLoader
     import im.vector.matrix.android.api.session.Session
    +import im.vector.riotx.ActiveSessionDataSource
    +import im.vector.riotx.core.glide.FactoryUrl
    +import java.io.InputStream
     import javax.inject.Inject
     
     /**
      * This class is used to configure the library we use for images
      */
     class ImageManager @Inject constructor(
    -        private val context: Context
    +        private val context: Context,
    +        private val activeSessionDataSource: ActiveSessionDataSource
     ) {
     
         fun onSessionStarted(session: Session) {
    -        BigImageViewer.initialize(GlideImageLoader.with(context))
    +        // Do this call first
    +        BigImageViewer.initialize(GlideImageLoader.with(context, session.getOkHttpClient()))
    +
    +        val glide = Glide.get(context)
    +
    +        // And this one. FIXME But are losing what BigImageViewer has done to add a Progress listener
    +        glide.registry.replace(GlideUrl::class.java, InputStream::class.java, FactoryUrl(activeSessionDataSource))
         }
     }
    diff --git a/vector/src/main/java/im/vector/riotx/core/glide/FactoryUrl.kt b/vector/src/main/java/im/vector/riotx/core/glide/FactoryUrl.kt
    new file mode 100644
    index 0000000000..fc037894db
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/riotx/core/glide/FactoryUrl.kt
    @@ -0,0 +1,38 @@
    +/*
    + * 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.riotx.core.glide
    +
    +import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
    +import com.bumptech.glide.load.model.GlideUrl
    +import com.bumptech.glide.load.model.ModelLoader
    +import com.bumptech.glide.load.model.ModelLoaderFactory
    +import com.bumptech.glide.load.model.MultiModelLoaderFactory
    +import im.vector.riotx.ActiveSessionDataSource
    +import okhttp3.OkHttpClient
    +import java.io.InputStream
    +
    +class FactoryUrl(private val activeSessionDataSource: ActiveSessionDataSource) : ModelLoaderFactory {
    +
    +    override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader {
    +        val client = activeSessionDataSource.currentValue?.orNull()?.getOkHttpClient() ?: OkHttpClient()
    +        return OkHttpUrlLoader(client)
    +    }
    +
    +    override fun teardown() {
    +        // Do nothing, this instance doesn't own the client.
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/riotx/core/glide/VectorGlideModelLoader.kt b/vector/src/main/java/im/vector/riotx/core/glide/VectorGlideModelLoader.kt
    index 191ab6d972..510eef71e1 100644
    --- a/vector/src/main/java/im/vector/riotx/core/glide/VectorGlideModelLoader.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/glide/VectorGlideModelLoader.kt
    @@ -65,7 +65,7 @@ class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolde
                                  private val height: Int)
         : DataFetcher {
     
    -    val client = OkHttpClient()
    +    private val client = activeSessionHolder.getSafeActiveSession()?.getOkHttpClient() ?: OkHttpClient()
     
         override fun getDataClass(): Class {
             return InputStream::class.java
    
    From d63f00851a26a88b7b6cced6aeb4b4ee94ee587b Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 3 Jul 2020 21:17:47 +0200
    Subject: [PATCH 39/41] Rename parameters
    
    ---
     .../vector/riotx/features/home/AvatarRenderer.kt | 16 ++++++++--------
     1 file changed, 8 insertions(+), 8 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
    index 687c280910..f917b5a9f9 100644
    --- a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
    @@ -65,19 +65,19 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
     
         @UiThread
         fun render(context: Context,
    -               glideRequest: GlideRequests,
    +               glideRequests: GlideRequests,
                    matrixItem: MatrixItem,
                    target: Target) {
             val placeholder = getPlaceholderDrawable(context, matrixItem)
    -        buildGlideRequest(glideRequest, matrixItem.avatarUrl)
    +        buildGlideRequest(glideRequests, matrixItem.avatarUrl)
                     .placeholder(placeholder)
                     .into(target)
         }
     
         @AnyThread
         @Throws
    -    fun shortcutDrawable(context: Context, glideRequest: GlideRequests, matrixItem: MatrixItem, iconSize: Int): Bitmap {
    -        return glideRequest
    +    fun shortcutDrawable(context: Context, glideRequests: GlideRequests, matrixItem: MatrixItem, iconSize: Int): Bitmap {
    +        return glideRequests
                     .asBitmap()
                     .apply {
                         val resolvedUrl = resolvedUrl(matrixItem.avatarUrl)
    @@ -98,8 +98,8 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
         }
     
         @AnyThread
    -    fun getCachedDrawable(glideRequest: GlideRequests, matrixItem: MatrixItem): Drawable {
    -        return buildGlideRequest(glideRequest, matrixItem.avatarUrl)
    +    fun getCachedDrawable(glideRequests: GlideRequests, matrixItem: MatrixItem): Drawable {
    +        return buildGlideRequest(glideRequests, matrixItem.avatarUrl)
                     .onlyRetrieveFromCache(true)
                     .submit()
                     .get()
    @@ -117,9 +117,9 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
     
         // PRIVATE API *********************************************************************************
     
    -    private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest {
    +    private fun buildGlideRequest(glideRequests: GlideRequests, avatarUrl: String?): GlideRequest {
             val resolvedUrl = resolvedUrl(avatarUrl)
    -        return glideRequest
    +        return glideRequests
                     .load(resolvedUrl)
                     .apply(RequestOptions.circleCropTransform())
         }
    
    From 51898a810937baa2285a926bded3de33f873e3cb Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 10 Jul 2020 15:06:34 +0200
    Subject: [PATCH 40/41] Create new strings for change translations
    
    ---
     .../im/vector/riotx/core/ui/views/KeysBackupBanner.kt     | 8 ++++----
     vector/src/main/res/layout/view_keys_backup_banner.xml    | 4 ++--
     vector/src/main/res/values/strings.xml                    | 7 +++++--
     3 files changed, 11 insertions(+), 8 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt
    index 252eab02a6..d0cea6194b 100755
    --- a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt
    +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt
    @@ -17,7 +17,6 @@
     package im.vector.riotx.core.ui.views
     
     import android.content.Context
    -import androidx.preference.PreferenceManager
     import android.util.AttributeSet
     import android.view.View
     import android.view.ViewGroup
    @@ -25,6 +24,7 @@ import android.widget.TextView
     import androidx.constraintlayout.widget.ConstraintLayout
     import androidx.core.content.edit
     import androidx.core.view.isVisible
    +import androidx.preference.PreferenceManager
     import androidx.transition.TransitionManager
     import butterknife.BindView
     import butterknife.ButterKnife
    @@ -160,9 +160,9 @@ class KeysBackupBanner @JvmOverloads constructor(
             } else {
                 isVisible = true
     
    -            textView1.setText(R.string.keys_backup_banner_setup_line1)
    +            textView1.setText(R.string.secure_backup_banner_setup_line1)
                 textView2.isVisible = true
    -            textView2.setText(R.string.keys_backup_banner_setup_line2)
    +            textView2.setText(R.string.secure_backup_banner_setup_line2)
                 close.isVisible = true
             }
         }
    @@ -195,7 +195,7 @@ class KeysBackupBanner @JvmOverloads constructor(
     
         private fun renderBackingUp() {
             isVisible = true
    -        textView1.setText(R.string.keys_backup_banner_setup_line1)
    +        textView1.setText(R.string.secure_backup_banner_setup_line1)
             textView2.isVisible = true
             textView2.setText(R.string.keys_backup_banner_in_progress)
             loading.isVisible = true
    diff --git a/vector/src/main/res/layout/view_keys_backup_banner.xml b/vector/src/main/res/layout/view_keys_backup_banner.xml
    index 4c3ec1da3f..6c8fc2b5a1 100644
    --- a/vector/src/main/res/layout/view_keys_backup_banner.xml
    +++ b/vector/src/main/res/layout/view_keys_backup_banner.xml
    @@ -34,7 +34,7 @@
             android:layout_height="wrap_content"
             android:layout_marginStart="27dp"
             android:layout_marginLeft="27dp"
    -        android:text="@string/keys_backup_banner_setup_line1"
    +        android:text="@string/secure_backup_banner_setup_line1"
             android:textColor="?riotx_text_primary"
             android:textSize="18sp"
             app:layout_constraintBottom_toTopOf="@id/view_keys_backup_banner_text_2"
    @@ -48,7 +48,7 @@
             android:layout_height="wrap_content"
             android:layout_marginStart="27dp"
             android:layout_marginLeft="27dp"
    -        android:text="@string/keys_backup_banner_setup_line2"
    +        android:text="@string/secure_backup_banner_setup_line2"
             android:textColor="?riotx_text_secondary"
             android:textSize="14sp"
             android:visibility="gone"
    diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
    index 8451191273..f34224b246 100644
    --- a/vector/src/main/res/values/strings.xml
    +++ b/vector/src/main/res/values/strings.xml
    @@ -1496,8 +1496,11 @@ Why choose Riot.im?
         It was me
     
         
    -    Secure Backup
    -    Safeguard against losing access to encrypted messages & data 
    +    Never lose encrypted messages
    +    Start using Key Backup
    +
    +    Secure Backup
    +    Safeguard against losing access to encrypted messages & data
     
         Never lose encrypted messages
         Use Key Backup
    
    From 179474b975a2628c8b108dd85e6cd06661cab51a Mon Sep 17 00:00:00 2001
    From: Benoit Marty 
    Date: Fri, 10 Jul 2020 17:51:57 +0200
    Subject: [PATCH 41/41] Cleanup
    
    ---
     .../riotx/features/reactions/widget/ReactionButton.kt  | 10 +++++-----
     1 file changed, 5 insertions(+), 5 deletions(-)
    
    diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt b/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt
    index ec5aba8ee5..140edaf03a 100644
    --- a/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt
    +++ b/vector/src/main/java/im/vector/riotx/features/reactions/widget/ReactionButton.kt
    @@ -45,7 +45,8 @@ import javax.inject.Inject
      * An animated reaction button.
      * Displays a String reaction (emoji), with a count, and that can be selected or not (toggle)
      */
    -class ReactionButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
    +class ReactionButton @JvmOverloads constructor(context: Context,
    +                                               attrs: AttributeSet? = null,
                                                    defStyleAttr: Int = 0)
         : FrameLayout(context, attrs, defStyleAttr), View.OnClickListener, View.OnLongClickListener {
     
    @@ -110,9 +111,7 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut
             countTextView?.text = TextUtils.formatCountToShortDecimal(reactionCount)
     
     //        emojiView?.typeface = this.emojiTypeFace ?: Typeface.DEFAULT
    -
             context.withStyledAttributes(attrs, R.styleable.ReactionButton, defStyleAttr) {
    -
                 onDrawable = ContextCompat.getDrawable(context, R.drawable.rounded_rect_shape)
                 offDrawable = ContextCompat.getDrawable(context, R.drawable.rounded_rect_shape_off)
     
    @@ -143,9 +142,10 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut
     
                 val status = getBoolean(R.styleable.ReactionButton_toggled, false)
                 setChecked(status)
    -            setOnClickListener(this@ReactionButton)
    -            setOnLongClickListener(this@ReactionButton)
             }
    +
    +        setOnClickListener(this)
    +        setOnLongClickListener(this)
         }
     
         private fun getDrawableFromResource(array: TypedArray, styleableIndexId: Int): Drawable? {