Merge branch 'develop' into feature/fix_small_issues

This commit is contained in:
ganfra 2020-07-10 20:13:47 +02:00
commit 3fc9fe3017
114 changed files with 4136 additions and 745 deletions

View File

@ -8,6 +8,7 @@ Improvements 🙌:
- Cleaning chunks with lots of events as long as a threshold has been exceeded (35_000 events in DB) (#1634) - Cleaning chunks with lots of events as long as a threshold has been exceeded (35_000 events in DB) (#1634)
- Creating and listening to EventInsertEntity. (#1634) - Creating and listening to EventInsertEntity. (#1634)
- Handling (almost) properly the groups fetching (#1634) - Handling (almost) properly the groups fetching (#1634)
- Improve fullscreen media display (#327)
Bugfix 🐛: Bugfix 🐛:
- Integration Manager: Wrong URL to review terms if URL in config contains path (#1606) - Integration Manager: Wrong URL to review terms if URL in config contains path (#1606)
@ -15,6 +16,8 @@ Bugfix 🐛:
- Bug / Unwanted draft (#698) - Bug / Unwanted draft (#698)
- All users seems to be able to see the enable encryption option in room settings (#1341) - All users seems to be able to see the enable encryption option in room settings (#1341)
- Leave room only leaves the current version (#1656) - Leave room only leaves the current version (#1656)
- Regression | Share action menu do not work (#1647)
- verification issues on transition (#1555)
Translations 🗣: Translations 🗣:
- -
@ -27,7 +30,7 @@ Build 🧱:
- Revert to build-tools 3.5.3 - Revert to build-tools 3.5.3
Other changes: Other changes:
- - Use `Context#withStyledAttributes` extension function (#1546)
Changes in Riot.imX 0.91.4 (2020-07-06) Changes in Riot.imX 0.91.4 (2020-07-06)
=================================================== ===================================================

1
attachment-viewer/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,78 @@
/*
* 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
defaultConfig {
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
}
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.chrisbanes:PhotoView:2.0.0'
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
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'
}

View File

21
attachment-viewer/proguard-rules.pro vendored Normal file
View File

@ -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

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="im.vector.riotx.attachmentviewer" />

View File

@ -0,0 +1,30 @@
/*
* 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 android.widget.ImageView
import android.widget.ProgressBar
class AnimatedImageViewHolder constructor(itemView: View) :
BaseViewHolder(itemView) {
val touchImageView: ImageView = itemView.findViewById(R.id.imageView)
val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress)
internal val target = DefaultImageLoaderTarget(this, this.touchImageView)
}

View File

@ -0,0 +1,31 @@
/*
* 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
sealed class AttachmentEvents {
data class VideoEvent(val isPlaying: Boolean, val progress: Int, val duration: Int) : AttachmentEvents()
}
interface AttachmentEventListener {
fun onEvent(event: AttachmentEvents)
}
sealed class AttachmentCommands {
object PauseVideo : AttachmentCommands()
object StartVideo : AttachmentCommands()
data class SeekTo(val percentProgress: Int) : AttachmentCommands()
}

View File

@ -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.content.Context
import android.view.View
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 {
fun getItemCount(): Int
fun getAttachmentInfoAt(position: Int): AttachmentInfo
fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.Image)
fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.AnimatedImage)
fun loadVideo(target: VideoLoaderTarget, info: AttachmentInfo.Video)
fun overlayViewAtPosition(context: Context, position: Int): View?
fun clear(id: String)
}

View File

@ -0,0 +1,335 @@
/*
* 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.
* 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.Color
import android.os.Bundle
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GestureDetectorCompat
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.transition.TransitionManager
import androidx.viewpager2.widget.ViewPager2
import kotlinx.android.synthetic.main.activity_attachment_viewer.*
import java.lang.ref.WeakReference
import kotlin.math.abs
abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventListener {
lateinit var pager2: ViewPager2
lateinit var imageTransitionView: ImageView
lateinit var transitionImageContainer: ViewGroup
var topInset = 0
var bottomInset = 0
var systemUiVisibility = true
private var overlayView: View? = null
set(value) {
if (value == overlayView) return
overlayView?.let { rootContainer.removeView(it) }
rootContainer.addView(value)
value?.updatePadding(top = topInset, bottom = bottomInset)
field = value
}
private lateinit var swipeDismissHandler: SwipeToDismissHandler
private lateinit var directionDetector: SwipeDirectionDetector
private lateinit var scaleDetector: ScaleGestureDetector
private lateinit var gestureDetector: GestureDetectorCompat
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 var isOverlayWasClicked = false
// 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)
// This is important for the dispatchTouchEvent, if not we must correct
// the touch coordinates
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_IMMERSIVE)
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
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()
gestureDetector = createGestureDetector()
attachmentPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageScrollStateChanged(state: Int) {
isImagePagerIdle = state == ViewPager2.SCROLL_STATE_IDLE
}
override fun onPageSelected(position: Int) {
onSelectedPositionChanged(position)
}
})
swipeDismissHandler = createSwipeToDismissHandler()
rootContainer.setOnTouchListener(swipeDismissHandler)
rootContainer.viewTreeObserver.addOnGlobalLayoutListener { swipeDismissHandler.translationLimit = dismissContainer.height / 4 }
scaleDetector = createScaleGestureDetector()
ViewCompat.setOnApplyWindowInsetsListener(rootContainer) { _, insets ->
overlayView?.updatePadding(top = insets.systemWindowInsetTop, bottom = insets.systemWindowInsetBottom)
topInset = insets.systemWindowInsetTop
bottomInset = insets.systemWindowInsetBottom
insets
}
}
fun onSelectedPositionChanged(position: Int) {
attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition)?.let {
(it as? BaseViewHolder)?.onSelected(false)
}
attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(position)?.let {
(it as? BaseViewHolder)?.onSelected(true)
if (it is VideoViewHolder) {
it.eventListener = WeakReference(this)
}
}
currentPosition = position
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
// Check if the overlay is visible, and wants to handle the click
if (overlayView?.isVisible == true && overlayView?.dispatchTouchEvent(ev) == 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 handleSingleTap(event: MotionEvent, isOverlayWasClicked: Boolean) {
// TODO if there is no overlay, we should at least toggle system bars?
if (overlayView != null && !isOverlayWasClicked) {
toggleOverlayViewVisibility()
super.dispatchTouchEvent(event)
}
}
private fun toggleOverlayViewVisibility() {
if (systemUiVisibility) {
// we hide
TransitionManager.beginDelayedTransition(rootContainer)
hideSystemUI()
overlayView?.isVisible = false
} else {
// we show
TransitionManager.beginDelayedTransition(rootContainer)
showSystemUI()
overlayView?.isVisible = true
}
}
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())
private fun createGestureDetector() =
GestureDetectorCompat(this, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
if (isImagePagerIdle) {
handleSingleTap(e, isOverlayWasClicked)
}
return false
}
override fun onDoubleTap(e: MotionEvent?): Boolean {
return super.onDoubleTap(e)
}
})
override fun onEvent(event: AttachmentEvents) {
if (overlayView is AttachmentEventListener) {
(overlayView as? AttachmentEventListener)?.onEvent(event)
}
}
protected open fun shouldAnimateDismiss(): Boolean = true
protected open fun animateClose() {
window.statusBarColor = Color.TRANSPARENT
finish()
}
fun handle(commands: AttachmentCommands) {
(attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition) as? BaseViewHolder)
?.handleCommand(commands)
}
private fun hideSystemUI() {
systemUiVisibility = false
// Enables regular immersive mode.
// For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE.
// Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
// Set the content to appear under the system bars so that the
// content doesn't resize when the system bars hide and show.
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
// Hide the nav bar and status bar
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN)
}
// Shows the system bars by removing all the flags
// except for the ones that make the content appear under the system bars.
private fun showSystemUI() {
systemUiVisibility = true
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
}
}

View File

@ -0,0 +1,115 @@
/*
* 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.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
class AttachmentsAdapter : RecyclerView.Adapter<BaseViewHolder>() {
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 -> ZoomableImageViewHolder(itemView)
R.layout.item_animated_image_attachment -> AnimatedImageViewHolder(itemView)
R.layout.item_video_attachment -> VideoViewHolder(itemView)
else -> UnsupportedViewHolder(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.AnimatedImage -> R.layout.item_animated_image_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)
when (it) {
is AttachmentInfo.Image -> {
attachmentSourceProvider?.loadImage((holder as ZoomableImageViewHolder).target, it)
}
is AttachmentInfo.AnimatedImage -> {
attachmentSourceProvider?.loadImage((holder as AnimatedImageViewHolder).target, it)
}
is AttachmentInfo.Video -> {
attachmentSourceProvider?.loadVideo((holder as VideoViewHolder).target, it)
}
// else -> {
// // }
}
}
}
override fun onViewAttachedToWindow(holder: BaseViewHolder) {
holder.onAttached()
}
override fun onViewRecycled(holder: BaseViewHolder) {
holder.onRecycled()
}
override fun onViewDetachedFromWindow(holder: BaseViewHolder) {
holder.onDetached()
}
fun isScaled(position: Int): Boolean {
val holder = recyclerView?.findViewHolderForAdapterPosition(position)
if (holder is ZoomableImageViewHolder) {
return holder.touchImageView.attacher.scale > 1f
}
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()
}
}

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -0,0 +1,39 @@
/*
* 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.
* 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
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
}
}
}
}

View File

@ -0,0 +1,91 @@
/*
* 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.
* 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.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()
}
}

View File

@ -0,0 +1,126 @@
/*
* 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.
* 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.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) }

View File

@ -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)
}
}

View File

@ -0,0 +1,157 @@
/*
* 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 android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.VideoView
import androidx.core.view.isVisible
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import java.io.File
import java.lang.ref.WeakReference
import java.util.concurrent.TimeUnit
// TODO, it would be probably better to use a unique media player
// for better customization and control
// But for now VideoView is enough, it released player when detached, we use a timer to update progress
class VideoViewHolder constructor(itemView: View) :
BaseViewHolder(itemView) {
private var isSelected = false
private var mVideoPath: String? = null
private var progressDisposable: Disposable? = null
private var progress: Int = 0
private var wasPaused = false
var eventListener: WeakReference<AttachmentEventListener>? = null
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)
internal val target = DefaultVideoLoaderTarget(this, thumbnailImage)
override fun onRecycled() {
super.onRecycled()
progressDisposable?.dispose()
progressDisposable = null
mVideoPath = null
}
fun videoReady(file: File) {
mVideoPath = file.path
if (isSelected) {
startPlaying()
}
}
fun videoFileLoadError() {
}
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()
} else {
progress = 0
}
progressDisposable?.dispose()
progressDisposable = null
} else {
if (mVideoPath != null) {
startPlaying()
}
}
isSelected = true
}
private fun startPlaying() {
thumbnailImage.isVisible = false
loaderProgressBar.isVisible = false
videoView.isVisible = true
videoView.setOnPreparedListener {
progressDisposable?.dispose()
progressDisposable = Observable.interval(100, TimeUnit.MILLISECONDS)
.timeInterval()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
val duration = videoView.duration
val progress = videoView.currentPosition
val isPlaying = videoView.isPlaying
// Log.v("FOO", "isPlaying $isPlaying $progress/$duration")
eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration))
}
}
videoView.setVideoPath(mVideoPath)
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()
}
is AttachmentCommands.SeekTo -> {
val duration = videoView.duration
if (duration > 0) {
val seekDuration = duration * (commands.percentProgress / 100f)
videoView.seekTo(seekDuration.toInt())
}
}
}
}
override fun bind(attachmentInfo: AttachmentInfo) {
super.bind(attachmentInfo)
progress = 0
wasPaused = false
}
}

View File

@ -0,0 +1,42 @@
/*
* 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 android.widget.ProgressBar
import com.github.chrisbanes.photoview.PhotoView
class ZoomableImageViewHolder 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)
}
internal val target = DefaultImageLoaderTarget.ZoomableImageTarget(this, touchImageView)
}

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rootContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".AttachmentViewerActivity">
<View
android:id="@+id/backgroundView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:alpha="1"
android:background="@android:color/black" />
<FrameLayout
android:id="@+id/dismissContainer"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/transitionImageContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="UselessParent"
tools:visibility="invisible">
<ImageView
android:id="@+id/transitionImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="ContentDescription" />
</FrameLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/attachmentPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:orientation="horizontal"
android:visibility="visible" />
</FrameLayout>
</FrameLayout>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">
<ImageView
android:id="@+id/imageView"
android:visibility="visible"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<ProgressBar
android:layout_centerInParent="true"
android:id="@+id/imageLoaderProgress"
style="?android:attr/progressBarStyle"
android:layout_width="40dp"
android:layout_height="40dp"
android:visibility="visible"
tools:visibility="gone" />
</RelativeLayout>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/touchImageView"
android:visibility="visible"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<ProgressBar
android:layout_centerInParent="true"
android:id="@+id/imageLoaderProgress"
style="?android:attr/progressBarStyle"
android:layout_width="40dp"
android:layout_height="40dp"
android:visibility="visible"
tools:visibility="gone" />
</RelativeLayout>

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">
<ImageView
android:id="@+id/videoThumbnailImage"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:scaleType="centerInside" />
<VideoView
android:id="@+id/videoView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true" />
<ImageView
android:id="@+id/videoControlIcon"
android:layout_centerInParent="true"
android:visibility="gone"
tools:visibility="visible"
android:layout_width="44dp"
android:layout_height="44dp"
/>
<ProgressBar
android:layout_centerInParent="true"
android:id="@+id/videoLoaderProgress"
style="?android:attr/progressBarStyle"
android:layout_width="40dp"
android:layout_height="40dp"
android:visibility="invisible"
tools:visibility="visible" />
<TextView
android:id="@+id/videoMediaViewerErrorView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_margin="16dp"
android:textSize="16sp"
android:visibility="gone"
tools:text="Error"
tools:visibility="visible" />
</RelativeLayout>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/design_default_color_primary">
<TextView
android:id="@+id/testPage"
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1"
android:textSize="80sp"
android:textStyle="bold" />
</RelativeLayout>

View File

@ -39,6 +39,8 @@ allprojects {
includeGroupByRegex "com\\.github\\.yalantis" includeGroupByRegex "com\\.github\\.yalantis"
// JsonViewer // JsonViewer
includeGroupByRegex 'com\\.github\\.BillCarsonFr' includeGroupByRegex 'com\\.github\\.BillCarsonFr'
// PhotoView
includeGroupByRegex 'com\\.github\\.chrisbanes'
} }
} }
maven { maven {

View File

@ -1,5 +1,6 @@
Useful links: Useful links:
- https://codelabs.developers.google.com/codelabs/webrtc-web/#0 - https://codelabs.developers.google.com/codelabs/webrtc-web/#0
- http://webrtc.github.io/webrtc-org/native-code/android/
╔════════════════════════════════════════════════╗ ╔════════════════════════════════════════════════╗

View File

@ -33,7 +33,7 @@ data class MatrixConfiguration(
), ),
/** /**
* Optional proxy to connect to the matrix servers * 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 val proxy: Proxy? = null
) { ) {

View File

@ -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.typing.TypingUsersTracker
import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.api.session.widgets.WidgetService import im.vector.matrix.android.api.session.widgets.WidgetService
import okhttp3.OkHttpClient
/** /**
* This interface defines interactions with a session. * This interface defines interactions with a session.
@ -205,6 +206,13 @@ interface Session :
*/ */
fun removeListener(listener: Listener) 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. * A global session listener to get notified for some events.
*/ */

View File

@ -61,6 +61,8 @@ interface CrossSigningService {
fun canCrossSign(): Boolean fun canCrossSign(): Boolean
fun allPrivateKeysKnown(): Boolean
fun trustUser(otherUserId: String, fun trustUser(otherUserId: String,
callback: MatrixCallback<Unit>) callback: MatrixCallback<Unit>)

View File

@ -39,4 +39,6 @@ interface TimelineService {
fun getTimeLineEvent(eventId: String): TimelineEvent? fun getTimeLineEvent(eventId: String): TimelineEvent?
fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>>
fun getAttachmentMessages() : List<TimelineEvent>
} }

View File

@ -507,6 +507,13 @@ internal class DefaultCrossSigningService @Inject constructor(
&& cryptoStore.getCrossSigningPrivateKeys()?.user != null && 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<Unit>) { override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
Timber.d("## CrossSigning - Mark user $userId as trusted ") Timber.d("## CrossSigning - Mark user $userId as trusted ")

View File

@ -1234,7 +1234,7 @@ internal class DefaultVerificationService @Inject constructor(
) )
// We can SCAN or SHOW QR codes only if cross-signing is enabled // We can SCAN or SHOW QR codes only if cross-signing is enabled
val methodValues = if (crossSigningService.isCrossSigningVerified()) { val methodValues = if (crossSigningService.isCrossSigningInitialized()) {
// Add reciprocate method if application declares it can scan or show QR codes // Add reciprocate method if application declares it can scan or show QR codes
// Not sure if it ok to do that (?) // Not sure if it ok to do that (?)
val reciprocateMethod = methods val reciprocateMethod = methods

View File

@ -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.crypto.DefaultCryptoService
import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.SessionId 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.di.WorkManagerProvider
import im.vector.matrix.android.internal.session.identity.DefaultIdentityService import im.vector.matrix.android.internal.session.identity.DefaultIdentityService
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecryptor 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 io.realm.RealmConfiguration
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
@ -113,8 +115,10 @@ internal class DefaultSession @Inject constructor(
private val defaultIdentityService: DefaultIdentityService, private val defaultIdentityService: DefaultIdentityService,
private val integrationManagerService: IntegrationManagerService, private val integrationManagerService: IntegrationManagerService,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val callSignalingService: Lazy<CallSignalingService>) private val callSignalingService: Lazy<CallSignalingService>,
: Session, @UnauthenticatedWithCertificate
private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>
) : Session,
RoomService by roomService.get(), RoomService by roomService.get(),
RoomDirectoryService by roomDirectoryService.get(), RoomDirectoryService by roomDirectoryService.get(),
GroupService by groupService.get(), GroupService by groupService.get(),
@ -255,6 +259,10 @@ internal class DefaultSession @Inject constructor(
override fun callSignalingService(): CallSignalingService = callSignalingService.get() override fun callSignalingService(): CallSignalingService = callSignalingService.get()
override fun getOkHttpClient(): OkHttpClient {
return unauthenticatedWithCertificateOkHttpClient.get()
}
override fun addListener(listener: Session.Listener) { override fun addListener(listener: Session.Listener) {
sessionListeners.addListener(listener) sessionListeners.addListener(listener)
} }

View File

@ -22,7 +22,7 @@ import javax.inject.Inject
internal class SessionListeners @Inject constructor() { internal class SessionListeners @Inject constructor() {
private val listeners = ArrayList<Session.Listener>() private val listeners = mutableSetOf<Session.Listener>()
fun addListener(listener: Session.Listener) { fun addListener(listener: Session.Listener) {
synchronized(listeners) { synchronized(listeners) {

View File

@ -21,19 +21,25 @@ import androidx.lifecycle.Transformations
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.isImageMessage
import im.vector.matrix.android.api.session.events.model.isVideoMessage
import im.vector.matrix.android.api.session.room.timeline.Timeline 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.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineService 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.session.room.timeline.TimelineSettings
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional 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.ReadReceiptsSummaryMapper
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper 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.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.database.query.where
import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.fetchCopyMap import im.vector.matrix.android.internal.util.fetchCopyMap
import io.realm.Sort
import io.realm.kotlin.where
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String, internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String,
@ -73,10 +79,10 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
override fun getTimeLineEvent(eventId: String): TimelineEvent? { override fun getTimeLineEvent(eventId: String): TimelineEvent? {
return monarchy return monarchy
.fetchCopyMap({ .fetchCopyMap({
TimelineEventEntity.where(it, roomId = roomId, eventId = eventId).findFirst() TimelineEventEntity.where(it, roomId = roomId, eventId = eventId).findFirst()
}, { entity, _ -> }, { entity, _ ->
timelineEventMapper.map(entity) timelineEventMapper.map(entity)
}) })
} }
override fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> { override fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> {
@ -88,4 +94,16 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
events.firstOrNull().toOptional() events.firstOrNull().toOptional()
} }
} }
override fun getAttachmentMessages(): List<TimelineEvent> {
// TODO pretty bad query.. maybe we should denormalize clear type in base?
return doWithRealm(monarchy.realmConfiguration) { realm ->
realm.where<TimelineEventEntity>()
.equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
.findAll()
?.mapNotNull { timelineEventMapper.map(it).takeIf { it.root.isImageMessage() || it.root.isVideoMessage() } }
?: emptyList()
}
}
} }

View File

@ -1,2 +1,6 @@
include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch' include ':vector'
include ':matrix-sdk-android'
include ':matrix-sdk-android-rx'
include ':diff-match-patch'
include ':attachment-viewer'
include ':multipicker' include ':multipicker'

View File

@ -279,6 +279,7 @@ dependencies {
implementation project(":matrix-sdk-android-rx") implementation project(":matrix-sdk-android-rx")
implementation project(":diff-match-patch") implementation project(":diff-match-patch")
implementation project(":multipicker") implementation project(":multipicker")
implementation project(":attachment-viewer")
implementation 'com.android.support:multidex:1.0.3' implementation 'com.android.support:multidex:1.0.3'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
@ -369,6 +370,10 @@ dependencies {
implementation "com.github.piasy:GlideImageLoader:$big_image_viewer_version" implementation "com.github.piasy:GlideImageLoader:$big_image_viewer_version"
implementation "com.github.piasy:ProgressPieIndicator:$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.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" implementation "com.github.bumptech.glide:glide:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version" kapt "com.github.bumptech.glide:compiler:$glide_version"
implementation 'com.danikula:videocache:2.7.1' implementation 'com.danikula:videocache:2.7.1'

View File

@ -85,6 +85,11 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".features.media.ImageMediaViewerActivity" /> <activity android:name=".features.media.ImageMediaViewerActivity" />
<activity
android:name=".features.media.VectorAttachmentViewerActivity"
android:theme="@style/AppTheme.Transparent" />
<activity android:name=".features.media.BigImageViewerActivity" /> <activity android:name=".features.media.BigImageViewerActivity" />
<activity <activity
android:name=".features.rageshake.BugReportActivity" android:name=".features.rageshake.BugReportActivity"

View File

@ -389,6 +389,9 @@ SOFTWARE.
<li> <li>
<b>BillCarsonFr/JsonViewer</b> <b>BillCarsonFr/JsonViewer</b>
</li> </li>
<li>
<b>Copyright (C) 2018 stfalcon.com</b>
</li>
</ul> </ul>
<pre> <pre>
Apache License Apache License

View File

@ -32,8 +32,6 @@ import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import com.facebook.stetho.Stetho import com.facebook.stetho.Stetho
import com.gabrielittner.threetenbp.LazyThreeTen 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.Matrix
import im.vector.matrix.android.api.MatrixConfiguration import im.vector.matrix.android.api.MatrixConfiguration
import im.vector.matrix.android.api.auth.AuthenticationService import im.vector.matrix.android.api.auth.AuthenticationService
@ -44,15 +42,12 @@ import im.vector.riotx.core.di.HasVectorInjector
import im.vector.riotx.core.di.VectorComponent import im.vector.riotx.core.di.VectorComponent
import im.vector.riotx.core.extensions.configureAndStart import im.vector.riotx.core.extensions.configureAndStart
import im.vector.riotx.core.rx.RxConfig 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.configuration.VectorConfiguration
import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks
import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.notifications.NotificationUtils 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.popup.PopupAlertManager
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler 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.settings.VectorPreferences
import im.vector.riotx.features.version.VersionProvider import im.vector.riotx.features.version.VersionProvider
import im.vector.riotx.push.fcm.FcmHelper import im.vector.riotx.push.fcm.FcmHelper
@ -79,16 +74,13 @@ class VectorApplication :
@Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper @Inject lateinit var emojiCompatWrapper: EmojiCompatWrapper
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
@Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var sessionListener: SessionListener
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener
@Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var versionProvider: VersionProvider @Inject lateinit var versionProvider: VersionProvider
@Inject lateinit var notificationUtils: NotificationUtils @Inject lateinit var notificationUtils: NotificationUtils
@Inject lateinit var appStateHandler: AppStateHandler @Inject lateinit var appStateHandler: AppStateHandler
@Inject lateinit var rxConfig: RxConfig @Inject lateinit var rxConfig: RxConfig
@Inject lateinit var popupAlertManager: PopupAlertManager @Inject lateinit var popupAlertManager: PopupAlertManager
@Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
lateinit var vectorComponent: VectorComponent lateinit var vectorComponent: VectorComponent
@ -114,7 +106,6 @@ class VectorApplication :
logInfo() logInfo()
LazyThreeTen.init(this) LazyThreeTen.init(this)
BigImageViewer.initialize(GlideImageLoader.with(applicationContext))
EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks(popupAlertManager)) registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks(popupAlertManager))
@ -137,8 +128,7 @@ class VectorApplication :
if (authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) { if (authenticationService.hasAuthenticatedSessions() && !activeSessionHolder.hasActiveSession()) {
val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!! val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
activeSessionHolder.setActiveSession(lastAuthenticatedSession) activeSessionHolder.setActiveSession(lastAuthenticatedSession)
lastAuthenticatedSession.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener) lastAuthenticatedSession.configureAndStart(applicationContext)
lastAuthenticatedSession.callSignalingService().addCallListener(webRtcPeerConnectionManager)
} }
ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver { ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME) @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright 2020 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -22,6 +22,7 @@ import android.graphics.drawable.ColorDrawable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.withStyledAttributes
import im.vector.riotx.R import im.vector.riotx.R
import kotlin.math.abs import kotlin.math.abs
@ -67,19 +68,19 @@ class PercentViewBehavior<V : View>(context: Context, attrs: AttributeSet) : Coo
private var isPrepared: Boolean = false private var isPrepared: Boolean = false
init { init {
val a = context.obtainStyledAttributes(attrs, R.styleable.PercentViewBehavior) context.withStyledAttributes(attrs, R.styleable.PercentViewBehavior) {
dependViewId = a.getResourceId(R.styleable.PercentViewBehavior_behavior_dependsOn, 0) dependViewId = getResourceId(R.styleable.PercentViewBehavior_behavior_dependsOn, 0)
dependType = a.getInt(R.styleable.PercentViewBehavior_behavior_dependType, DEPEND_TYPE_WIDTH) dependType = getInt(R.styleable.PercentViewBehavior_behavior_dependType, DEPEND_TYPE_WIDTH)
dependTarget = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_dependTarget, UNSPECIFIED_INT) dependTarget = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_dependTarget, UNSPECIFIED_INT)
targetX = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetX, UNSPECIFIED_INT) targetX = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetX, UNSPECIFIED_INT)
targetY = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetY, UNSPECIFIED_INT) targetY = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetY, UNSPECIFIED_INT)
targetWidth = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetWidth, UNSPECIFIED_INT) targetWidth = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetWidth, UNSPECIFIED_INT)
targetHeight = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetHeight, UNSPECIFIED_INT) targetHeight = getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetHeight, UNSPECIFIED_INT)
targetBackgroundColor = a.getColor(R.styleable.PercentViewBehavior_behavior_targetBackgroundColor, UNSPECIFIED_INT) targetBackgroundColor = getColor(R.styleable.PercentViewBehavior_behavior_targetBackgroundColor, UNSPECIFIED_INT)
targetAlpha = a.getFloat(R.styleable.PercentViewBehavior_behavior_targetAlpha, UNSPECIFIED_FLOAT) targetAlpha = getFloat(R.styleable.PercentViewBehavior_behavior_targetAlpha, UNSPECIFIED_FLOAT)
targetRotateX = a.getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateX, UNSPECIFIED_FLOAT) targetRotateX = getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateX, UNSPECIFIED_FLOAT)
targetRotateY = a.getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateY, UNSPECIFIED_FLOAT) targetRotateY = getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateY, UNSPECIFIED_FLOAT)
a.recycle() }
} }
private fun prepare(parent: CoordinatorLayout, child: View, dependency: View) { private fun prepare(parent: CoordinatorLayout, child: View, dependency: View) {

View File

@ -20,8 +20,12 @@ import arrow.core.Option
import im.vector.matrix.android.api.auth.AuthenticationService import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.riotx.ActiveSessionDataSource 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.keysrequest.KeyRequestHandler
import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler 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 java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -30,23 +34,42 @@ import javax.inject.Singleton
class ActiveSessionHolder @Inject constructor(private val authenticationService: AuthenticationService, class ActiveSessionHolder @Inject constructor(private val authenticationService: AuthenticationService,
private val sessionObservableStore: ActiveSessionDataSource, private val sessionObservableStore: ActiveSessionDataSource,
private val keyRequestHandler: KeyRequestHandler, 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 val imageManager: ImageManager
) { ) {
private var activeSession: AtomicReference<Session?> = AtomicReference() private var activeSession: AtomicReference<Session?> = AtomicReference()
fun setActiveSession(session: Session) { fun setActiveSession(session: Session) {
Timber.w("setActiveSession of ${session.myUserId}")
activeSession.set(session) activeSession.set(session)
sessionObservableStore.post(Option.just(session)) sessionObservableStore.post(Option.just(session))
keyRequestHandler.start(session) keyRequestHandler.start(session)
incomingVerificationRequestHandler.start(session) incomingVerificationRequestHandler.start(session)
session.addListener(sessionListener)
pushRuleTriggerListener.startWithSession(session)
session.callSignalingService().addCallListener(webRtcPeerConnectionManager)
imageManager.onSessionStarted(session)
} }
fun clearActiveSession() { fun clearActiveSession() {
// Do some cleanup first
getSafeActiveSession()?.let {
Timber.w("clearActiveSession of ${it.myUserId}")
it.callSignalingService().removeCallListener(webRtcPeerConnectionManager)
it.removeListener(sessionListener)
}
activeSession.set(null) activeSession.set(null)
sessionObservableStore.post(Option.empty()) sessionObservableStore.post(Option.empty())
keyRequestHandler.stop() keyRequestHandler.stop()
incomingVerificationRequestHandler.stop() incomingVerificationRequestHandler.stop()
pushRuleTriggerListener.stop()
} }
fun hasActiveSession(): Boolean { fun hasActiveSession(): Boolean {

View File

@ -0,0 +1,47 @@
/*
* 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.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 activeSessionDataSource: ActiveSessionDataSource
) {
fun onSessionStarted(session: Session) {
// 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))
}
}

View File

@ -48,6 +48,7 @@ import im.vector.riotx.features.invite.InviteUsersToRoomActivity
import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.invite.VectorInviteView
import im.vector.riotx.features.link.LinkHandlerActivity import im.vector.riotx.features.link.LinkHandlerActivity
import im.vector.riotx.features.login.LoginActivity import im.vector.riotx.features.login.LoginActivity
import im.vector.riotx.features.media.VectorAttachmentViewerActivity
import im.vector.riotx.features.media.BigImageViewerActivity import im.vector.riotx.features.media.BigImageViewerActivity
import im.vector.riotx.features.media.ImageMediaViewerActivity import im.vector.riotx.features.media.ImageMediaViewerActivity
import im.vector.riotx.features.media.VideoMediaViewerActivity import im.vector.riotx.features.media.VideoMediaViewerActivity
@ -72,6 +73,7 @@ import im.vector.riotx.features.terms.ReviewTermsActivity
import im.vector.riotx.features.ui.UiStateRepository import im.vector.riotx.features.ui.UiStateRepository
import im.vector.riotx.features.widgets.WidgetActivity import im.vector.riotx.features.widgets.WidgetActivity
import im.vector.riotx.features.widgets.permissions.RoomWidgetPermissionBottomSheet import im.vector.riotx.features.widgets.permissions.RoomWidgetPermissionBottomSheet
import im.vector.riotx.features.workers.signout.SignOutBottomSheetDialogFragment
@Component( @Component(
dependencies = [ dependencies = [
@ -135,6 +137,7 @@ interface ScreenComponent {
fun inject(activity: ReviewTermsActivity) fun inject(activity: ReviewTermsActivity)
fun inject(activity: WidgetActivity) fun inject(activity: WidgetActivity)
fun inject(activity: VectorCallActivity) fun inject(activity: VectorCallActivity)
fun inject(activity: VectorAttachmentViewerActivity)
/* ========================================================================================== /* ==========================================================================================
* BottomSheets * BottomSheets
@ -152,6 +155,7 @@ interface ScreenComponent {
fun inject(bottomSheet: RoomWidgetPermissionBottomSheet) fun inject(bottomSheet: RoomWidgetPermissionBottomSheet)
fun inject(bottomSheet: RoomWidgetsBottomSheet) fun inject(bottomSheet: RoomWidgetsBottomSheet)
fun inject(bottomSheet: CallControlsBottomSheet) fun inject(bottomSheet: CallControlsBottomSheet)
fun inject(bottomSheet: SignOutBottomSheetDialogFragment)
/* ========================================================================================== /* ==========================================================================================
* Others * Others

View File

@ -36,7 +36,6 @@ import im.vector.riotx.features.reactions.EmojiChooserViewModel
import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel
import im.vector.riotx.features.roomprofile.RoomProfileSharedActionViewModel import im.vector.riotx.features.roomprofile.RoomProfileSharedActionViewModel
import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel import im.vector.riotx.features.userdirectory.UserDirectorySharedActionViewModel
import im.vector.riotx.features.workers.signout.SignOutViewModel
@Module @Module
interface ViewModelModule { interface ViewModelModule {
@ -51,11 +50,6 @@ interface ViewModelModule {
* Below are bindings for the androidx view models (which extend ViewModel). Will be converted to MvRx ViewModel in the future. * 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 @Binds
@IntoMap @IntoMap
@ViewModelKey(EmojiChooserViewModel::class) @ViewModelKey(EmojiChooserViewModel::class)

View File

@ -16,9 +16,16 @@
package im.vector.riotx.core.extensions package im.vector.riotx.core.extensions
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Parcelable import android.os.Parcelable
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment 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) { fun VectorBaseFragment.addFragment(frameId: Int, fragment: Fragment) {
parentFragmentManager.commitTransaction { add(frameId, fragment) } parentFragmentManager.commitTransaction { add(frameId, fragment) }
@ -89,3 +96,29 @@ fun Fragment.getAllChildFragments(): List<Fragment> {
// Define a missing constant // Define a missing constant
const val POP_BACK_STACK_EXCLUSIVE = 0 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)
}
// }
}

View File

@ -24,20 +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.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.riotx.core.services.VectorSyncService import im.vector.riotx.core.services.VectorSyncService
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.session.SessionListener
import timber.log.Timber import timber.log.Timber
fun Session.configureAndStart(context: Context, fun Session.configureAndStart(context: Context) {
pushRuleTriggerListener: PushRuleTriggerListener, Timber.i("Configure and start session for $myUserId")
sessionListener: SessionListener) {
open() open()
addListener(sessionListener)
setFilter(FilterService.FilterPreset.RiotFilter) setFilter(FilterService.FilterPreset.RiotFilter)
Timber.i("Configure and start session for ${this.myUserId}")
startSyncing(context) startSyncing(context)
refreshPushers() refreshPushers()
pushRuleTriggerListener.startWithSession(this)
} }
fun Session.startSyncing(context: Context) { fun Session.startSyncing(context: Context) {
@ -65,3 +59,12 @@ fun Session.hasUnsavedKeys(): Boolean {
return cryptoService().inboundGroupSessionsCount(false) > 0 return cryptoService().inboundGroupSessionsCount(false) > 0
&& cryptoService().keysBackupService().state != KeysBackupState.ReadyToBackUp && 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())
}

View File

@ -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<GlideUrl, InputStream> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<GlideUrl, InputStream> {
val client = activeSessionDataSource.currentValue?.orNull()?.getOkHttpClient() ?: OkHttpClient()
return OkHttpUrlLoader(client)
}
override fun teardown() {
// Do nothing, this instance doesn't own the client.
}
}

View File

@ -65,7 +65,7 @@ class VectorGlideDataFetcher(private val activeSessionHolder: ActiveSessionHolde
private val height: Int) private val height: Int)
: DataFetcher<InputStream> { : DataFetcher<InputStream> {
val client = OkHttpClient() private val client = activeSessionHolder.getSafeActiveSession()?.getOkHttpClient() ?: OkHttpClient()
override fun getDataClass(): Class<InputStream> { override fun getDataClass(): Class<InputStream> {
return InputStream::class.java return InputStream::class.java

View File

@ -38,6 +38,7 @@ import android.text.TextUtils.substring
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.util.AttributeSet import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.AppCompatTextView
import androidx.core.content.withStyledAttributes
import timber.log.Timber import timber.log.Timber
import java.util.ArrayList import java.util.ArrayList
import java.util.regex.Pattern import java.util.regex.Pattern
@ -71,6 +72,7 @@ class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: Att
private var maxLines = 0 private var maxLines = 0
private var lineSpacingMult = 1.0f private var lineSpacingMult = 1.0f
private var lineAddVertPad = 0.0f private var lineAddVertPad = 0.0f
/** /**
* The end punctuation which will be removed when appending [.ELLIPSIS]. * The end punctuation which will be removed when appending [.ELLIPSIS].
*/ */
@ -408,9 +410,9 @@ class EllipsizingTextView @JvmOverloads constructor(context: Context, attrs: Att
} }
init { init {
val a = context.obtainStyledAttributes(attrs, intArrayOf(android.R.attr.maxLines, android.R.attr.ellipsize), defStyle, 0) context.withStyledAttributes(attrs, intArrayOf(android.R.attr.maxLines, android.R.attr.ellipsize), defStyle) {
maxLines = a.getInt(0, Int.MAX_VALUE) maxLines = getInt(0, Int.MAX_VALUE)
a.recycle() }
setEndPunctuationPattern(DEFAULT_END_PUNCTUATION) setEndPunctuationPattern(DEFAULT_END_PUNCTUATION)
val currentTextColor = currentTextColor val currentTextColor = currentTextColor
val ellipsizeColor = Color.argb(ELLIPSIZE_ALPHA, Color.red(currentTextColor), Color.green(currentTextColor), Color.blue(currentTextColor)) val ellipsizeColor = Color.argb(ELLIPSIZE_ALPHA, Color.red(currentTextColor), Color.green(currentTextColor), Color.blue(currentTextColor))

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright 2020 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,6 +18,7 @@ package im.vector.riotx.core.platform
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import androidx.core.content.withStyledAttributes
import androidx.core.widget.NestedScrollView import androidx.core.widget.NestedScrollView
import im.vector.riotx.R import im.vector.riotx.R
@ -34,9 +35,9 @@ class MaxHeightScrollView @JvmOverloads constructor(context: Context, attrs: Att
init { init {
if (attrs != null) { if (attrs != null) {
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView) context.withStyledAttributes(attrs, R.styleable.MaxHeightScrollView) {
maxHeight = styledAttrs.getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT) maxHeight = getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT)
styledAttrs.recycle() }
} }
} }

View File

@ -25,6 +25,7 @@ import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.content.withStyledAttributes
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -117,16 +118,15 @@ class BottomSheetActionButton @JvmOverloads constructor(
inflate(context, R.layout.item_verification_action, this) inflate(context, R.layout.item_verification_action, this)
ButterKnife.bind(this) ButterKnife.bind(this)
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.BottomSheetActionButton, 0, 0) context.withStyledAttributes(attrs, R.styleable.BottomSheetActionButton) {
title = typedArray.getString(R.styleable.BottomSheetActionButton_actionTitle) ?: "" title = getString(R.styleable.BottomSheetActionButton_actionTitle) ?: ""
subTitle = typedArray.getString(R.styleable.BottomSheetActionButton_actionDescription) ?: "" subTitle = getString(R.styleable.BottomSheetActionButton_actionDescription) ?: ""
forceStartPadding = typedArray.getBoolean(R.styleable.BottomSheetActionButton_forceStartPadding, false) forceStartPadding = getBoolean(R.styleable.BottomSheetActionButton_forceStartPadding, false)
leftIcon = typedArray.getDrawable(R.styleable.BottomSheetActionButton_leftIcon) 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)) tint = getColor(R.styleable.BottomSheetActionButton_tint, ThemeUtils.getColor(context, android.R.attr.textColor))
}
typedArray.recycle()
} }
} }

View File

@ -17,15 +17,14 @@
package im.vector.riotx.core.ui.views package im.vector.riotx.core.ui.views
import android.content.Context import android.content.Context
import androidx.preference.PreferenceManager
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AbsListView
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.preference.PreferenceManager
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import butterknife.BindView import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
@ -58,22 +57,12 @@ class KeysBackupBanner @JvmOverloads constructor(
var delegate: Delegate? = null var delegate: Delegate? = null
private var state: State = State.Initial 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 { init {
setupView() setupView()
PreferenceManager.getDefaultSharedPreferences(context).edit {
putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false)
putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, "")
}
} }
/** /**
@ -91,7 +80,8 @@ class KeysBackupBanner @JvmOverloads constructor(
state = newState state = newState
hideAll() hideAll()
val parent = parent as ViewGroup
TransitionManager.beginDelayedTransition(parent)
when (newState) { when (newState) {
State.Initial -> renderInitial() State.Initial -> renderInitial()
State.Hidden -> renderHidden() State.Hidden -> renderHidden()
@ -102,22 +92,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?) { override fun onClick(v: View?) {
when (state) { when (state) {
is State.Setup -> { is State.Setup -> {
@ -166,6 +140,8 @@ class KeysBackupBanner @JvmOverloads constructor(
ButterKnife.bind(this) ButterKnife.bind(this)
setOnClickListener(this) setOnClickListener(this)
textView1.setOnClickListener(this)
textView2.setOnClickListener(this)
} }
private fun renderInitial() { private fun renderInitial() {
@ -184,9 +160,9 @@ class KeysBackupBanner @JvmOverloads constructor(
} else { } else {
isVisible = true isVisible = true
textView1.setText(R.string.keys_backup_banner_setup_line1) textView1.setText(R.string.secure_backup_banner_setup_line1)
textView2.isVisible = true textView2.isVisible = true
textView2.setText(R.string.keys_backup_banner_setup_line2) textView2.setText(R.string.secure_backup_banner_setup_line2)
close.isVisible = true close.isVisible = true
} }
} }
@ -218,10 +194,10 @@ class KeysBackupBanner @JvmOverloads constructor(
} }
private fun renderBackingUp() { private fun renderBackingUp() {
// Do not render when backing up anymore isVisible = true
isVisible = false textView1.setText(R.string.secure_backup_banner_setup_line1)
textView2.isVisible = true
textView1.setText(R.string.keys_backup_banner_in_progress) textView2.setText(R.string.keys_backup_banner_in_progress)
loading.isVisible = true loading.isVisible = true
} }

View File

@ -36,6 +36,9 @@ open class BehaviorDataSource<T>(private val defaultValue: T? = null) : MutableD
private val behaviorRelay = createRelay() private val behaviorRelay = createRelay()
val currentValue: T?
get() = behaviorRelay.value
override fun observe(): Observable<T> { override fun observe(): Observable<T> {
return behaviorRelay.hide().observeOn(AndroidSchedulers.mainThread()) return behaviorRelay.hide().observeOn(AndroidSchedulers.mainThread())
} }

View File

@ -22,6 +22,7 @@ import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.extensions.tryThis 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.CallState
import im.vector.matrix.android.api.session.call.CallsListener import im.vector.matrix.android.api.session.call.CallsListener
import im.vector.matrix.android.api.session.call.EglUtils 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.CallCandidatesContent
import im.vector.matrix.android.api.session.room.model.call.CallHangupContent 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.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.BluetoothHeadsetReceiver
import im.vector.riotx.core.services.CallService import im.vector.riotx.core.services.CallService
import im.vector.riotx.core.services.WiredHeadsetStateReceiver import im.vector.riotx.core.services.WiredHeadsetStateReceiver
@ -71,9 +72,12 @@ import javax.inject.Singleton
@Singleton @Singleton
class WebRtcPeerConnectionManager @Inject constructor( class WebRtcPeerConnectionManager @Inject constructor(
private val context: Context, private val context: Context,
private val sessionHolder: ActiveSessionHolder private val activeSessionDataSource: ActiveSessionDataSource
) : CallsListener { ) : CallsListener {
private val currentSession: Session?
get() = activeSessionDataSource.currentValue?.orNull()
interface CurrentCallListener { interface CurrentCallListener {
fun onCurrentCallChange(call: MxCall?) fun onCurrentCallChange(call: MxCall?)
fun onCaptureStateChanged(mgr: WebRtcPeerConnectionManager) {} fun onCaptureStateChanged(mgr: WebRtcPeerConnectionManager) {}
@ -288,15 +292,16 @@ class WebRtcPeerConnectionManager @Inject constructor(
} }
private fun getTurnServer(callback: ((TurnServerResponse?) -> Unit)) { private fun getTurnServer(callback: ((TurnServerResponse?) -> Unit)) {
sessionHolder.getActiveSession().callSignalingService().getTurnServer(object : MatrixCallback<TurnServerResponse?> { currentSession?.callSignalingService()
override fun onSuccess(data: TurnServerResponse?) { ?.getTurnServer(object : MatrixCallback<TurnServerResponse?> {
callback(data) override fun onSuccess(data: TurnServerResponse?) {
} callback(data)
}
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
callback(null) callback(null)
} }
}) })
} }
fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) { fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) {
@ -310,7 +315,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
currentCall?.mxCall currentCall?.mxCall
?.takeIf { it.state is CallState.Connected } ?.takeIf { it.state is CallState.Connected }
?.let { mxCall -> ?.let { mxCall ->
val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName() val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
?: mxCall.roomId ?: mxCall.roomId
// Start background service with notification // Start background service with notification
CallService.onPendingCall( CallService.onPendingCall(
@ -318,7 +323,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
isVideo = mxCall.isVideoCall, isVideo = mxCall.isVideoCall,
roomName = name, roomName = name,
roomId = mxCall.roomId, roomId = mxCall.roomId,
matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "", matrixId = currentSession?.myUserId ?: "",
callId = mxCall.callId) callId = mxCall.callId)
} }
@ -373,14 +378,14 @@ class WebRtcPeerConnectionManager @Inject constructor(
val mxCall = callContext.mxCall val mxCall = callContext.mxCall
// Update service state // Update service state
val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName() val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
?: mxCall.roomId ?: mxCall.roomId
CallService.onPendingCall( CallService.onPendingCall(
context = context, context = context,
isVideo = mxCall.isVideoCall, isVideo = mxCall.isVideoCall,
roomName = name, roomName = name,
roomId = mxCall.roomId, roomId = mxCall.roomId,
matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "", matrixId = currentSession?.myUserId ?: "",
callId = mxCall.callId callId = mxCall.callId
) )
executor.execute { executor.execute {
@ -563,14 +568,14 @@ class WebRtcPeerConnectionManager @Inject constructor(
?.let { mxCall -> ?.let { mxCall ->
// Start background service with notification // Start background service with notification
val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName() val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
?: mxCall.otherUserId ?: mxCall.otherUserId
CallService.onOnGoingCallBackground( CallService.onOnGoingCallBackground(
context = context, context = context,
isVideo = mxCall.isVideoCall, isVideo = mxCall.isVideoCall,
roomName = name, roomName = name,
roomId = mxCall.roomId, roomId = mxCall.roomId,
matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "", matrixId = currentSession?.myUserId ?: "",
callId = mxCall.callId callId = mxCall.callId
) )
} }
@ -631,20 +636,20 @@ class WebRtcPeerConnectionManager @Inject constructor(
} }
Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall") 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) val callContext = CallContext(createdCall)
audioManager.startForCall(createdCall) audioManager.startForCall(createdCall)
currentCall = callContext currentCall = callContext
val name = sessionHolder.getSafeActiveSession()?.getUser(createdCall.otherUserId)?.getBestName() val name = currentSession?.getUser(createdCall.otherUserId)?.getBestName()
?: createdCall.otherUserId ?: createdCall.otherUserId
CallService.onOutgoingCallRinging( CallService.onOutgoingCallRinging(
context = context.applicationContext, context = context.applicationContext,
isVideo = createdCall.isVideoCall, isVideo = createdCall.isVideoCall,
roomName = name, roomName = name,
roomId = createdCall.roomId, roomId = createdCall.roomId,
matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "", matrixId = currentSession?.myUserId ?: "",
callId = createdCall.callId) callId = createdCall.callId)
executor.execute { executor.execute {
@ -693,14 +698,14 @@ class WebRtcPeerConnectionManager @Inject constructor(
} }
// Start background service with notification // Start background service with notification
val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName() val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
?: mxCall.otherUserId ?: mxCall.otherUserId
CallService.onIncomingCallRinging( CallService.onIncomingCallRinging(
context = context, context = context,
isVideo = mxCall.isVideoCall, isVideo = mxCall.isVideoCall,
roomName = name, roomName = name,
roomId = mxCall.roomId, roomId = mxCall.roomId,
matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "", matrixId = currentSession?.myUserId ?: "",
callId = mxCall.callId callId = mxCall.callId
) )
@ -818,14 +823,14 @@ class WebRtcPeerConnectionManager @Inject constructor(
} }
val mxCall = call.mxCall val mxCall = call.mxCall
// Update service state // Update service state
val name = sessionHolder.getSafeActiveSession()?.getUser(mxCall.otherUserId)?.getBestName() val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName()
?: mxCall.otherUserId ?: mxCall.otherUserId
CallService.onPendingCall( CallService.onPendingCall(
context = context, context = context,
isVideo = mxCall.isVideoCall, isVideo = mxCall.isVideoCall,
roomName = name, roomName = name,
roomId = mxCall.roomId, roomId = mxCall.roomId,
matrixId = sessionHolder.getSafeActiveSession()?.myUserId ?: "", matrixId = currentSession?.myUserId ?: "",
callId = mxCall.callId callId = mxCall.callId
) )
executor.execute { executor.execute {

View File

@ -17,37 +17,34 @@
package im.vector.riotx.features.crypto.keys package im.vector.riotx.features.crypto.keys
import android.content.Context 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.MatrixCallback
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.awaitCallback 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.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File
class KeysExporter(private val session: Session) { class KeysExporter(private val session: Session) {
/** /**
* Export keys and return the file path with the callback * Export keys and return the file path with the callback
*/ */
fun export(context: Context, password: String, callback: MatrixCallback<String>) { fun export(context: Context, password: String, uri: Uri, callback: MatrixCallback<Boolean>) {
GlobalScope.launch(Dispatchers.Main) { GlobalScope.launch(Dispatchers.Main) {
runCatching { runCatching {
val data = awaitCallback<ByteArray> { session.cryptoService().exportRoomKeys(password, it) }
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val parentDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) val data = awaitCallback<ByteArray> { session.cryptoService().exportRoomKeys(password, it) }
val file = File(parentDir, "riotx-keys-" + System.currentTimeMillis() + ".txt") val os = context.contentResolver?.openOutputStream(uri)
if (os == null) {
writeToFile(data, file) false
} else {
addEntryToDownloadManager(context, file, "text/plain") os.write(data)
os.flush()
file.absolutePath true
}
} }
}.foldToCallback(callback) }.foldToCallback(callback)
} }

View File

@ -15,6 +15,8 @@
*/ */
package im.vector.riotx.features.crypto.keysbackup.setup package im.vector.riotx.features.crypto.keysbackup.setup
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
@ -132,36 +134,22 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
this, this,
PERMISSION_REQUEST_CODE_EXPORT_KEYS, PERMISSION_REQUEST_CODE_EXPORT_KEYS,
R.string.permissions_rationale_msg_keys_backup_export)) { R.string.permissions_rationale_msg_keys_backup_export)) {
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener { try {
override fun onPassphrase(passphrase: String) { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
showWaitingView() intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TITLE, "riot-megolm-export-${session.myUserId}-${System.currentTimeMillis()}.txt")
KeysExporter(session) startActivityForResult(
.export(this@KeysBackupSetupActivity, Intent.createChooser(
passphrase, intent,
object : MatrixCallback<String> { getString(R.string.keys_backup_setup_step1_manual_export)
override fun onSuccess(data: String) { ),
hideWaitingView() REQUEST_CODE_SAVE_MEGOLM_EXPORT
)
AlertDialog.Builder(this@KeysBackupSetupActivity) } catch (activityNotFoundException: ActivityNotFoundException) {
.setMessage(getString(R.string.encryption_export_saved_as, data)) toast(R.string.error_no_external_application_found)
.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()
}
})
}
})
} }
} }
@ -173,6 +161,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<Boolean> {
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() { override fun onBackPressed() {
if (viewModel.shouldPromptOnBack) { if (viewModel.shouldPromptOnBack) {
if (waitingView?.isVisible == true) { if (waitingView?.isVisible == true) {
@ -205,6 +234,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
const val KEYS_VERSION = "KEYS_VERSION" const val KEYS_VERSION = "KEYS_VERSION"
const val MANUAL_EXPORT = "MANUAL_EXPORT" const val MANUAL_EXPORT = "MANUAL_EXPORT"
const val EXTRA_SHOW_MANUAL_EXPORT = "SHOW_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 { fun intent(context: Context, showManualExport: Boolean): Intent {
val intent = Intent(context, KeysBackupSetupActivity::class.java) val intent = Intent(context, KeysBackupSetupActivity::class.java)

View File

@ -15,13 +15,13 @@
*/ */
package im.vector.riotx.features.crypto.keysbackup.setup package im.vector.riotx.features.crypto.keysbackup.setup
import android.os.AsyncTask
import android.os.Bundle import android.os.Bundle
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.EditText import android.widget.EditText
import android.widget.ImageView import android.widget.ImageView
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.viewModelScope
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import butterknife.BindView import butterknife.BindView
import butterknife.OnClick 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.platform.VectorBaseFragment
import im.vector.riotx.core.ui.views.PasswordStrengthBar import im.vector.riotx.core.ui.views.PasswordStrengthBar
import im.vector.riotx.features.settings.VectorLocale import im.vector.riotx.features.settings.VectorLocale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment() { class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment() {
@ -117,9 +119,9 @@ class KeysBackupSetupStep2Fragment @Inject constructor() : VectorBaseFragment()
if (newValue.isEmpty()) { if (newValue.isEmpty()) {
viewModel.passwordStrength.value = null viewModel.passwordStrength.value = null
} else { } else {
AsyncTask.execute { viewModel.viewModelScope.launch(Dispatchers.IO) {
val strength = zxcvbn.measure(newValue) val strength = zxcvbn.measure(newValue)
activity?.runOnUiThread { launch(Dispatchers.Main) {
viewModel.passwordStrength.value = strength viewModel.passwordStrength.value = strength
} }
} }

View File

@ -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.crosssigning.toBase64NoPadding
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo 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.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.keysbackup.util.extractCurveKeyFromRecoveryKey
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.android.internal.util.awaitCallback import im.vector.matrix.android.internal.util.awaitCallback
@ -84,8 +85,10 @@ class BootstrapCrossSigningTask @Inject constructor(
override suspend fun execute(params: Params): BootstrapResult { override suspend fun execute(params: Params): BootstrapResult {
val crossSigningService = session.cryptoService().crossSigningService() 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 // Ensure cross-signing is initialized. Due to migration it is maybe not always correctly initialized
if (!crossSigningService.isCrossSigningInitialized()) { if (!crossSigningService.isCrossSigningInitialized()) {
Timber.d("## BootstrapCrossSigningTask: Cross signing not enabled, so initialize")
params.progressListener?.onProgress( params.progressListener?.onProgress(
WaitingViewData( WaitingViewData(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing), stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing),
@ -104,8 +107,9 @@ class BootstrapCrossSigningTask @Inject constructor(
return handleInitializeXSigningError(failure) return handleInitializeXSigningError(failure)
} }
} else { } else {
// not sure how this can happen?? Timber.d("## BootstrapCrossSigningTask: Cross signing already setup, go to 4S setup")
if (params.initOnlyCrossSigning) { if (params.initOnlyCrossSigning) {
// not sure how this can happen??
return handleInitializeXSigningError(IllegalArgumentException("Cross signing already setup")) return handleInitializeXSigningError(IllegalArgumentException("Cross signing already setup"))
} }
} }
@ -119,6 +123,8 @@ class BootstrapCrossSigningTask @Inject constructor(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_pbkdf2), stringProvider.getString(R.string.bootstrap_crosssigning_progress_pbkdf2),
isIndeterminate = true) isIndeterminate = true)
) )
Timber.d("## BootstrapCrossSigningTask: Creating 4S key with pass: ${params.passphrase != null}")
try { try {
keyInfo = awaitCallback { keyInfo = awaitCallback {
params.passphrase?.let { passphrase -> params.passphrase?.let { passphrase ->
@ -141,6 +147,7 @@ class BootstrapCrossSigningTask @Inject constructor(
} }
} }
} catch (failure: Failure) { } catch (failure: Failure) {
Timber.e("## BootstrapCrossSigningTask: Creating 4S - Failed to generate key <${failure.localizedMessage}>")
return BootstrapResult.FailedToCreateSSSSKey(failure) return BootstrapResult.FailedToCreateSSSSKey(failure)
} }
@ -149,19 +156,24 @@ class BootstrapCrossSigningTask @Inject constructor(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_default_key), stringProvider.getString(R.string.bootstrap_crosssigning_progress_default_key),
isIndeterminate = true) isIndeterminate = true)
) )
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Set default key")
try { try {
awaitCallback<Unit> { awaitCallback<Unit> {
ssssService.setDefaultKey(keyInfo.keyId, it) ssssService.setDefaultKey(keyInfo.keyId, it)
} }
} catch (failure: Failure) { } catch (failure: Failure) {
// Maybe we could just ignore this error? // Maybe we could just ignore this error?
Timber.e("## BootstrapCrossSigningTask: Creating 4S - Set default key error <${failure.localizedMessage}>")
return BootstrapResult.FailedToSetDefaultSSSSKey(failure) return BootstrapResult.FailedToSetDefaultSSSSKey(failure)
} }
Timber.d("## BootstrapCrossSigningTask: Creating 4S - gathering private keys")
val xKeys = crossSigningService.getCrossSigningPrivateKeys() val xKeys = crossSigningService.getCrossSigningPrivateKeys()
val mskPrivateKey = xKeys?.master ?: return BootstrapResult.MissingPrivateKey val mskPrivateKey = xKeys?.master ?: return BootstrapResult.MissingPrivateKey
val sskPrivateKey = xKeys.selfSigned ?: return BootstrapResult.MissingPrivateKey val sskPrivateKey = xKeys.selfSigned ?: return BootstrapResult.MissingPrivateKey
val uskPrivateKey = xKeys.user ?: return BootstrapResult.MissingPrivateKey val uskPrivateKey = xKeys.user ?: return BootstrapResult.MissingPrivateKey
Timber.d("## BootstrapCrossSigningTask: Creating 4S - gathering private keys success")
try { try {
params.progressListener?.onProgress( params.progressListener?.onProgress(
@ -170,6 +182,7 @@ class BootstrapCrossSigningTask @Inject constructor(
isIndeterminate = true isIndeterminate = true
) )
) )
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing MSK...")
awaitCallback<Unit> { awaitCallback<Unit> {
ssssService.storeSecret( ssssService.storeSecret(
MASTER_KEY_SSSS_NAME, MASTER_KEY_SSSS_NAME,
@ -183,6 +196,7 @@ class BootstrapCrossSigningTask @Inject constructor(
isIndeterminate = true isIndeterminate = true
) )
) )
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing USK...")
awaitCallback<Unit> { awaitCallback<Unit> {
ssssService.storeSecret( ssssService.storeSecret(
USER_SIGNING_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME,
@ -196,6 +210,7 @@ class BootstrapCrossSigningTask @Inject constructor(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_ssk), isIndeterminate = true stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_ssk), isIndeterminate = true
) )
) )
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing SSK...")
awaitCallback<Unit> { awaitCallback<Unit> {
ssssService.storeSecret( ssssService.storeSecret(
SELF_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME,
@ -204,6 +219,7 @@ class BootstrapCrossSigningTask @Inject constructor(
) )
} }
} catch (failure: Failure) { } catch (failure: Failure) {
Timber.e("## BootstrapCrossSigningTask: Creating 4S - Failed to store keys <${failure.localizedMessage}>")
// Maybe we could just ignore this error? // Maybe we could just ignore this error?
return BootstrapResult.FailedToStorePrivateKeyInSSSS(failure) return BootstrapResult.FailedToStorePrivateKeyInSSSS(failure)
} }
@ -215,7 +231,14 @@ class BootstrapCrossSigningTask @Inject constructor(
) )
) )
try { try {
if (session.cryptoService().keysBackupService().keysBackupVersion == null) { Timber.d("## BootstrapCrossSigningTask: Creating 4S - Checking megolm backup")
// First ensure that in sync
val serverVersion = awaitCallback<KeysVersionResult?> {
session.cryptoService().keysBackupService().getCurrentVersion(it)
}
if (serverVersion == null) {
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Create megolm backup")
val creationInfo = awaitCallback<MegolmBackupCreationInfo> { val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
} }
@ -223,6 +246,7 @@ class BootstrapCrossSigningTask @Inject constructor(
session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it) session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
} }
// Save it for gossiping // Save it for gossiping
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Save megolm backup key for gossiping")
session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
awaitCallback<Unit> { awaitCallback<Unit> {
@ -239,6 +263,7 @@ class BootstrapCrossSigningTask @Inject constructor(
Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup") Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup")
} }
Timber.d("## BootstrapCrossSigningTask: initXSOnly:${params.initOnlyCrossSigning} Finished")
return BootstrapResult.Success(keyInfo) return BootstrapResult.Success(keyInfo)
} }

View File

@ -406,7 +406,10 @@ class BootstrapSharedViewModel @AssistedInject constructor(
setState { setState {
copy( copy(
recoveryKeyCreationInfo = bootstrapResult.keyInfo, recoveryKeyCreationInfo = bootstrapResult.keyInfo,
step = BootstrapStep.SaveRecoveryKey(false) step = BootstrapStep.SaveRecoveryKey(
// If a passphrase was used, saving key is optional
state.passphrase != null
)
) )
} }
} }

View File

@ -250,7 +250,10 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
is VerificationTxState.Started, is VerificationTxState.Started,
is VerificationTxState.WaitingOtherReciprocateConfirm -> { is VerificationTxState.WaitingOtherReciprocateConfirm -> {
showFragment(VerificationQRWaitingFragment::class, Bundle().apply { showFragment(VerificationQRWaitingFragment::class, Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationQRWaitingFragment.Args(state.isMe, state.otherUserMxItem?.getBestName() ?: "")) putParcelable(MvRx.KEY_ARG, VerificationQRWaitingFragment.Args(
isMe = state.isMe,
otherUserName = state.otherUserMxItem?.getBestName() ?: ""
))
}) })
return@withState return@withState
} }
@ -353,6 +356,17 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
} }
} }
} }
fun forSelfVerification(session: Session, outgoingRequest: String): VerificationBottomSheet {
return VerificationBottomSheet().apply {
arguments = Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationArgs(
otherUserId = session.myUserId,
selfVerificationMode = true,
verificationId = outgoingRequest
))
}
}
}
const val WAITING_SELF_VERIF_TAG: String = "WAITING_SELF_VERIF_TAG" const val WAITING_SELF_VERIF_TAG: String = "WAITING_SELF_VERIF_TAG"
} }

View File

@ -65,19 +65,19 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
@UiThread @UiThread
fun render(context: Context, fun render(context: Context,
glideRequest: GlideRequests, glideRequests: GlideRequests,
matrixItem: MatrixItem, matrixItem: MatrixItem,
target: Target<Drawable>) { target: Target<Drawable>) {
val placeholder = getPlaceholderDrawable(context, matrixItem) val placeholder = getPlaceholderDrawable(context, matrixItem)
buildGlideRequest(glideRequest, matrixItem.avatarUrl) buildGlideRequest(glideRequests, matrixItem.avatarUrl)
.placeholder(placeholder) .placeholder(placeholder)
.into(target) .into(target)
} }
@AnyThread @AnyThread
@Throws @Throws
fun shortcutDrawable(context: Context, glideRequest: GlideRequests, matrixItem: MatrixItem, iconSize: Int): Bitmap { fun shortcutDrawable(context: Context, glideRequests: GlideRequests, matrixItem: MatrixItem, iconSize: Int): Bitmap {
return glideRequest return glideRequests
.asBitmap() .asBitmap()
.apply { .apply {
val resolvedUrl = resolvedUrl(matrixItem.avatarUrl) val resolvedUrl = resolvedUrl(matrixItem.avatarUrl)
@ -98,8 +98,8 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
} }
@AnyThread @AnyThread
fun getCachedDrawable(glideRequest: GlideRequests, matrixItem: MatrixItem): Drawable { fun getCachedDrawable(glideRequests: GlideRequests, matrixItem: MatrixItem): Drawable {
return buildGlideRequest(glideRequest, matrixItem.avatarUrl) return buildGlideRequest(glideRequests, matrixItem.avatarUrl)
.onlyRetrieveFromCache(true) .onlyRetrieveFromCache(true)
.submit() .submit()
.get() .get()
@ -117,9 +117,9 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
// PRIVATE API ********************************************************************************* // PRIVATE API *********************************************************************************
private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest<Drawable> { private fun buildGlideRequest(glideRequests: GlideRequests, avatarUrl: String?): GlideRequest<Drawable> {
val resolvedUrl = resolvedUrl(avatarUrl) val resolvedUrl = resolvedUrl(avatarUrl)
return glideRequest return glideRequests
.load(resolvedUrl) .load(resolvedUrl)
.apply(RequestOptions.circleCropTransform()) .apply(RequestOptions.circleCropTransform())
} }

View File

@ -46,7 +46,8 @@ import im.vector.riotx.features.popup.PopupAlertManager
import im.vector.riotx.features.popup.VerificationVectorAlert import im.vector.riotx.features.popup.VerificationVectorAlert
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.settings.VectorPreferences 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 im.vector.riotx.push.fcm.FcmHelper
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity_home.* import kotlinx.android.synthetic.main.activity_home.*
@ -60,13 +61,16 @@ data class HomeActivityArgs(
val accountCreation: Boolean val accountCreation: Boolean
) : Parcelable ) : Parcelable
class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory { class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory, ServerBackupStatusViewModel.Factory {
private lateinit var sharedActionViewModel: HomeSharedActionViewModel private lateinit var sharedActionViewModel: HomeSharedActionViewModel
private val homeActivityViewModel: HomeActivityViewModel by viewModel() private val homeActivityViewModel: HomeActivityViewModel by viewModel()
@Inject lateinit var viewModelFactory: HomeActivityViewModel.Factory @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 activeSessionHolder: ActiveSessionHolder
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
@Inject lateinit var pushManager: PushersManager @Inject lateinit var pushManager: PushersManager
@ -92,6 +96,10 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
return unknownDeviceViewModelFactory.create(initialState) return unknownDeviceViewModelFactory.create(initialState)
} }
override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel {
return serverBackupviewModelFactory.create(initialState)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice()) FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice())
@ -177,7 +185,11 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
R.string.crosssigning_verify_this_session, R.string.crosssigning_verify_this_session,
R.string.confirm_your_identity R.string.confirm_your_identity
) { ) {
it.navigator.waitSessionVerification(it) if (event.waitForIncomingRequest) {
it.navigator.waitSessionVerification(it)
} else {
it.navigator.requestSelfSessionVerification(it)
}
} }
} }
@ -230,7 +242,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
} }
// Force remote backup state update to update the banner if needed // Force remote backup state update to update the banner if needed
viewModelProvider.get(SignOutViewModel::class.java).refreshRemoteStateIfNeeded() serverBackupStatusViewModel.refreshRemoteStateIfNeeded()
} }
override fun configure(toolbar: Toolbar) { override fun configure(toolbar: Toolbar) {

View File

@ -21,5 +21,5 @@ import im.vector.riotx.core.platform.VectorViewEvents
sealed class HomeActivityViewEvents : VectorViewEvents { sealed class HomeActivityViewEvents : VectorViewEvents {
data class AskPasswordToInitCrossSigning(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents() data class AskPasswordToInitCrossSigning(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents()
data class OnNewSession(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents() data class OnNewSession(val userItem: MatrixItem.UserItem?, val waitForIncomingRequest: Boolean = true) : HomeActivityViewEvents()
} }

View File

@ -130,7 +130,14 @@ class HomeActivityViewModel @AssistedInject constructor(
// Cross-signing is already set up for this user, is it trusted? // Cross-signing is already set up for this user, is it trusted?
if (!mxCrossSigningInfo.isTrusted()) { if (!mxCrossSigningInfo.isTrusted()) {
// New session // New session
_viewEvents.post(HomeActivityViewEvents.OnNewSession(session.getUser(session.myUserId)?.toMatrixItem())) _viewEvents.post(
HomeActivityViewEvents.OnNewSession(
session.getUser(session.myUserId)?.toMatrixItem(),
// If it's an old unverified, we should send requests
// instead of waiting for an incoming one
reAuthHelper.data != null
)
)
} }
} else { } else {
// Initialize cross-signing // Initialize cross-signing

View File

@ -27,7 +27,6 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.bottomnavigation.BottomNavigationItemView import com.google.android.material.bottomnavigation.BottomNavigationItemView
import com.google.android.material.bottomnavigation.BottomNavigationMenuView 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.session.group.model.GroupSummary
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
@ -50,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.PopupAlertManager
import im.vector.riotx.features.popup.VerificationVectorAlert 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.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.*
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 timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -66,15 +62,17 @@ private const val INDEX_ROOMS = 2
class HomeDetailFragment @Inject constructor( class HomeDetailFragment @Inject constructor(
val homeDetailViewModelFactory: HomeDetailViewModel.Factory, val homeDetailViewModelFactory: HomeDetailViewModel.Factory,
private val serverBackupStatusViewModelFactory: ServerBackupStatusViewModel.Factory,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val alertManager: PopupAlertManager, private val alertManager: PopupAlertManager,
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager
) : VectorBaseFragment(), KeysBackupBanner.Delegate, ActiveCallView.Callback { ) : VectorBaseFragment(), KeysBackupBanner.Delegate, ActiveCallView.Callback, ServerBackupStatusViewModel.Factory {
private val unreadCounterBadgeViews = arrayListOf<UnreadCounterBadgeView>() private val unreadCounterBadgeViews = arrayListOf<UnreadCounterBadgeView>()
private val viewModel: HomeDetailViewModel by fragmentViewModel() private val viewModel: HomeDetailViewModel by fragmentViewModel()
private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel() private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel()
private val serverBackupStatusViewModel: ServerBackupStatusViewModel by activityViewModel()
private lateinit var sharedActionViewModel: HomeSharedActionViewModel private lateinit var sharedActionViewModel: HomeSharedActionViewModel
private lateinit var sharedCallActionViewModel: SharedActiveCallViewModel private lateinit var sharedCallActionViewModel: SharedActiveCallViewModel
@ -196,34 +194,14 @@ class HomeDetailFragment @Inject constructor(
} }
private fun setupKeysBackupBanner() { private fun setupKeysBackupBanner() {
// Keys backup banner serverBackupStatusViewModel.subscribe(this) {
// Use the SignOutViewModel, it observe the keys backup state and this is what we need here when (val banState = it.bannerState.invoke()) {
val model = fragmentViewModelProvider.get(SignOutViewModel::class.java) is BannerState.Setup -> homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false)
BannerState.BackingUp -> homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false)
model.keysBackupState.observe(viewLifecycleOwner, Observer { keysBackupState -> null,
when (keysBackupState) { BannerState.Hidden -> homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false)
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)
} }
}) }.disposeOnDestroyView()
homeKeysBackupBanner.delegate = this homeKeysBackupBanner.delegate = this
} }
@ -332,4 +310,8 @@ class HomeDetailFragment @Inject constructor(
} }
} }
} }
override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel {
return serverBackupStatusViewModelFactory.create(initialState)
}
} }

View File

@ -1174,14 +1174,27 @@ class RoomDetailFragment @Inject constructor(
} }
override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) { override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) {
navigator.openImageViewer(requireActivity(), 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(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: "")) pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
} }
} }
override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) { override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) {
navigator.openVideoViewer(requireActivity(), mediaData) 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 onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) { // override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) {
@ -1199,7 +1212,7 @@ class RoomDetailFragment @Inject constructor(
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (allGranted(grantResults)) { if (allGranted(grantResults)) {
when (requestCode) { when (requestCode) {
SAVE_ATTACHEMENT_REQUEST_CODE -> { SAVE_ATTACHEMENT_REQUEST_CODE -> {
sharedActionViewModel.pendingAction?.let { sharedActionViewModel.pendingAction?.let {
handleActions(it) handleActions(it)
sharedActionViewModel.pendingAction = null sharedActionViewModel.pendingAction = null
@ -1340,13 +1353,13 @@ class RoomDetailFragment @Inject constructor(
private fun onShareActionClicked(action: EventSharedAction.Share) { private fun onShareActionClicked(action: EventSharedAction.Share) {
session.fileService().downloadFile( session.fileService().downloadFile(
FileService.DownloadMode.FOR_EXTERNAL_SHARE, downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
action.eventId, id = action.eventId,
action.messageContent.body, fileName = action.messageContent.body,
action.messageContent.getFileUrl(), mimeType = action.messageContent.mimeType,
action.messageContent.mimeType, url = action.messageContent.getFileUrl(),
action.messageContent.encryptedFileInfo?.toElementToDecrypt(), elementToDecrypt = action.messageContent.encryptedFileInfo?.toElementToDecrypt(),
object : MatrixCallback<File> { callback = object : MatrixCallback<File> {
override fun onSuccess(data: File) { override fun onSuccess(data: File) {
if (isAdded) { if (isAdded) {
shareMedia(requireContext(), data, getMimeTypeFromUri(requireContext(), data.toUri())) shareMedia(requireContext(), data, getMimeTypeFromUri(requireContext(), data.toUri()))

View File

@ -877,13 +877,13 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
} else { } else {
session.fileService().downloadFile( session.fileService().downloadFile(
FileService.DownloadMode.FOR_INTERNAL_USE, downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
action.eventId, id = action.eventId,
action.messageFileContent.getFileName(), fileName = action.messageFileContent.getFileName(),
action.messageFileContent.mimeType, mimeType = action.messageFileContent.mimeType,
mxcUrl, url = mxcUrl,
action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(), elementToDecrypt = action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(),
object : MatrixCallback<File> { callback = object : MatrixCallback<File> {
override fun onSuccess(data: File) { override fun onSuccess(data: File) {
_viewEvents.post(RoomDetailViewEvents.DownloadFileState( _viewEvents.post(RoomDetailViewEvents.DownloadFileState(
action.messageFileContent.mimeType, action.messageFileContent.mimeType,

View File

@ -337,7 +337,7 @@ class MessageItemFactory @Inject constructor(
.playable(true) .playable(true)
.highlighted(highlight) .highlighted(highlight)
.mediaData(thumbnailData) .mediaData(thumbnailData)
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) } .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view.findViewById(R.id.messageThumbnailView)) }
} }
private fun buildItemForTextContent(messageContent: MessageTextContent, private fun buildItemForTextContent(messageContent: MessageTextContent,

View File

@ -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.api.session.widgets.model.WidgetContent
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
import im.vector.riotx.ActiveSessionDataSource
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject 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 roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
private val sp: StringProvider) { 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? { fun format(timelineEvent: TimelineEvent): CharSequence? {
return when (val type = timelineEvent.root.getClearType()) { 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 ?: "" val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: event.stateKey ?: ""
return when (eventContent?.membership) { return when (eventContent?.membership) {
Membership.INVITE -> { Membership.INVITE -> {
val selfUserId = sessionHolder.getSafeActiveSession()?.myUserId
when { when {
eventContent.thirdPartyInvite != null -> { eventContent.thirdPartyInvite != null -> {
val userWhoHasAccepted = eventContent.thirdPartyInvite?.signed?.mxid ?: event.stateKey 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) sp.getString(R.string.notice_room_third_party_registered_invite, userWhoHasAccepted, threePidDisplayName)
} }
} }
event.stateKey == selfUserId -> event.stateKey == currentUserId ->
eventContent.safeReason?.let { reason -> eventContent.safeReason?.let { reason ->
sp.getString(R.string.notice_room_invite_you_with_reason, senderDisplayName, reason) sp.getString(R.string.notice_room_invite_you_with_reason, senderDisplayName, reason)
} ?: sp.getString(R.string.notice_room_invite_you, senderDisplayName) } ?: sp.getString(R.string.notice_room_invite_you, senderDisplayName)

View File

@ -22,6 +22,7 @@ import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.content.withStyledAttributes
import butterknife.BindView import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
import im.vector.riotx.R import im.vector.riotx.R
@ -73,11 +74,11 @@ class PollResultLineView @JvmOverloads constructor(
orientation = HORIZONTAL orientation = HORIZONTAL
ButterKnife.bind(this) ButterKnife.bind(this)
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.PollResultLineView, 0, 0) context.withStyledAttributes(attrs, R.styleable.PollResultLineView) {
label = typedArray.getString(R.styleable.PollResultLineView_optionName) ?: "" label = getString(R.styleable.PollResultLineView_optionName) ?: ""
percent = typedArray.getString(R.styleable.PollResultLineView_optionCount) ?: "" percent = getString(R.styleable.PollResultLineView_optionCount) ?: ""
optionSelected = typedArray.getBoolean(R.styleable.PollResultLineView_optionSelected, false) optionSelected = getBoolean(R.styleable.PollResultLineView_optionSelected, false)
isWinner = typedArray.getBoolean(R.styleable.PollResultLineView_optionIsWinner, false) isWinner = getBoolean(R.styleable.PollResultLineView_optionIsWinner, false)
typedArray.recycle() }
} }
} }

View File

@ -49,9 +49,6 @@ import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.ensureTrailingSlash 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 im.vector.riotx.features.signout.soft.SoftLogoutActivity
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.CancellationException import java.util.concurrent.CancellationException
@ -64,13 +61,10 @@ class LoginViewModel @AssistedInject constructor(
private val applicationContext: Context, private val applicationContext: Context,
private val authenticationService: AuthenticationService, private val authenticationService: AuthenticationService,
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
private val pushRuleTriggerListener: PushRuleTriggerListener,
private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory, private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory,
private val sessionListener: SessionListener,
private val reAuthHelper: ReAuthHelper, private val reAuthHelper: ReAuthHelper,
private val stringProvider: StringProvider, private val stringProvider: StringProvider
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager) ) : VectorViewModel<LoginViewState, LoginAction, LoginViewEvents>(initialState) {
: VectorViewModel<LoginViewState, LoginAction, LoginViewEvents>(initialState) {
@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {
@ -667,8 +661,7 @@ class LoginViewModel @AssistedInject constructor(
private fun onSessionCreated(session: Session) { private fun onSessionCreated(session: Session) {
activeSessionHolder.setActiveSession(session) activeSessionHolder.setActiveSession(session)
session.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener) session.configureAndStart(applicationContext)
session.callSignalingService().addCallListener(webRtcPeerConnectionManager)
setState { setState {
copy( copy(
asyncLoginAction = Success(Unit) asyncLoginAction = Success(Unit)

View File

@ -0,0 +1,107 @@
/*
* 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.SeekBar
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.Group
import im.vector.riotx.R
import im.vector.riotx.attachmentviewer.AttachmentEventListener
import im.vector.riotx.attachmentviewer.AttachmentEvents
class AttachmentOverlayView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), AttachmentEventListener {
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
private val shareImage: ImageView
private val overlayPlayPauseButton: ImageView
private val overlaySeekBar: SeekBar
var isPlaying = false
val videoControlsGroup: Group
var suspendSeekBarUpdate = false
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)
videoControlsGroup = findViewById(R.id.overlayVideoControlsGroup)
overlayPlayPauseButton = findViewById(R.id.overlayPlayPauseButton)
overlaySeekBar = findViewById(R.id.overlaySeekBar)
findViewById<ImageView>(R.id.overlayBackButton).setOnClickListener {
onBack?.invoke()
}
findViewById<ImageView>(R.id.overlayShareButton).setOnClickListener {
onShareCallback?.invoke()
}
findViewById<ImageView>(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) {
counterTextView.text = counter
infoTextView.text = senderInfo
}
override fun onEvent(event: AttachmentEvents) {
when (event) {
is AttachmentEvents.VideoEvent -> {
overlayPlayPauseButton.setImageResource(if (!event.isPlaying) R.drawable.ic_play_arrow else R.drawable.ic_pause)
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
}
}
}
}
}

View File

@ -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<ImageView, Drawable>(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<in Drawable>?) {
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<ImageView, Drawable>(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<in Drawable>?) {
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<ImageView, Drawable>(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<in Drawable>?) {
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<File> {
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))
}

View File

@ -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<AttachmentData>,
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<File> {
override fun onSuccess(data: File) {
callback(data)
}
override fun onFailure(failure: Throwable) {
callback(null)
}
}
)
}
}

View File

@ -19,11 +19,13 @@ package im.vector.riotx.features.media
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import android.view.View
import android.widget.ImageView import android.widget.ImageView
import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.CustomViewTarget
import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.target.Target
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.ORIENTATION_USE_EXIF import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.ORIENTATION_USE_EXIF
import com.github.piasy.biv.view.BigImageView import com.github.piasy.biv.view.BigImageView
@ -42,21 +44,29 @@ import java.io.File
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.min import kotlin.math.min
interface AttachmentData : Parcelable {
val eventId: String
val filename: String
val mimeType: String?
val url: String?
val elementToDecrypt: ElementToDecrypt?
}
class ImageContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, class ImageContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val dimensionConverter: DimensionConverter) { private val dimensionConverter: DimensionConverter) {
@Parcelize @Parcelize
data class Data( data class Data(
val eventId: String, override val eventId: String,
val filename: String, override val filename: String,
val mimeType: String?, override val mimeType: String?,
val url: String?, override val url: String?,
val elementToDecrypt: ElementToDecrypt?, override val elementToDecrypt: ElementToDecrypt?,
val height: Int?, val height: Int?,
val maxHeight: Int, val maxHeight: Int,
val width: Int?, val width: Int?,
val maxWidth: Int val maxWidth: Int
) : Parcelable { ) : AttachmentData {
fun isLocalFile() = url.isLocalFile() fun isLocalFile() = url.isLocalFile()
} }
@ -93,6 +103,25 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
.into(imageView) .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) { fun renderFitTarget(data: Data, mode: Mode, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) {
val size = processSize(data, mode) val size = processSize(data, mode)
@ -122,6 +151,45 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
.into(imageView) .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<Drawable> {
override fun onLoadFailed(e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean): Boolean {
callback?.invoke(false)
return false
}
override fun onResourceReady(resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
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<Drawable> { private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest<Drawable> {
return if (data.elementToDecrypt != null) { return if (data.elementToDecrypt != null) {
// Encrypted image // Encrypted image

View File

@ -91,6 +91,8 @@ class ImageMediaViewerActivity : VectorBaseActivity() {
encryptedImageView.isVisible = false encryptedImageView.isVisible = false
// Postpone transaction a bit until thumbnail is loaded // Postpone transaction a bit until thumbnail is loaded
supportPostponeEnterTransition() supportPostponeEnterTransition()
// We are not passing the exact same image that in the
imageContentRenderer.renderFitTarget(mediaData, ImageContentRenderer.Mode.THUMBNAIL, imageTransitionView) { imageContentRenderer.renderFitTarget(mediaData, ImageContentRenderer.Mode.THUMBNAIL, imageTransitionView) {
// Proceed with transaction // Proceed with transaction
scheduleStartPostponedTransition(imageTransitionView) scheduleStartPostponedTransition(imageTransitionView)
@ -134,13 +136,13 @@ class ImageMediaViewerActivity : VectorBaseActivity() {
private fun onShareActionClicked() { private fun onShareActionClicked() {
session.fileService().downloadFile( session.fileService().downloadFile(
FileService.DownloadMode.FOR_EXTERNAL_SHARE, downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
mediaData.eventId, id = mediaData.eventId,
mediaData.filename, fileName = mediaData.filename,
mediaData.mimeType, mimeType = mediaData.mimeType,
mediaData.url, url = mediaData.url,
mediaData.elementToDecrypt, elementToDecrypt = mediaData.elementToDecrypt,
object : MatrixCallback<File> { callback = object : MatrixCallback<File> {
override fun onSuccess(data: File) { override fun onSuccess(data: File) {
shareMedia(this@ImageMediaViewerActivity, data, getMimeTypeFromUri(this@ImageMediaViewerActivity, data.toUri())) shareMedia(this@ImageMediaViewerActivity, data, getMimeTypeFromUri(this@ImageMediaViewerActivity, data.toUri()))
} }

View File

@ -0,0 +1,175 @@
/*
* 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.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
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.attachmentviewer.AttachmentInfo
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.extensions.localDateTime
import java.io.File
import javax.inject.Inject
class RoomEventsAttachmentProvider(
private val attachments: List<TimelineEvent>,
private val initialIndex: Int,
imageContentRenderer: ImageContentRenderer,
private val dateFormatter: VectorDateFormatter,
fileService: FileService
) : BaseAttachmentProvider(imageContentRenderer, fileService) {
override fun getItemCount(): Int {
return attachments.size
}
override fun getAttachmentInfoAt(position: Int): AttachmentInfo {
return attachments[position].let {
val content = it.root.getClearContent().toModel<MessageContent>() as? MessageWithAttachmentContent
if (content is MessageImageContent) {
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
)
if (content.mimeType == "image/gif") {
AttachmentInfo.AnimatedImage(
uid = it.eventId,
url = content.url ?: "",
data = data
)
} else {
AttachmentInfo.Image(
uid = it.eventId,
url = content.url ?: "",
data = data
)
}
} else if (content 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
)
val data = VideoContentRenderer.Data(
eventId = it.eventId,
filename = content.body,
mimeType = content.mimeType,
url = content.getFileUrl(),
elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),
thumbnailMediaData = thumbnailData
)
AttachmentInfo.Video(
uid = it.eventId,
url = content.getFileUrl() ?: "",
data = data,
thumbnail = AttachmentInfo.Image(
uid = it.eventId,
url = content.videoInfo?.thumbnailFile?.url
?: content.videoInfo?.thumbnailUrl ?: "",
data = thumbnailData
)
)
} else {
AttachmentInfo.Image(
uid = it.eventId,
url = "",
data = null
)
}
}
}
override fun overlayViewAtPosition(context: Context, position: Int): View? {
super.overlayViewAtPosition(context, position)
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")
overlayView?.videoControlsGroup?.isVisible = item.root.isVideoMessage()
return overlayView
}
override fun getFileForSharing(position: Int, callback: (File?) -> Unit) {
attachments[position].let { timelineEvent ->
val messageContent = timelineEvent.root.getClearContent().toModel<MessageContent>()
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<File> {
override fun onSuccess(data: File) {
callback(data)
}
override fun onFailure(failure: Throwable) {
callback(null)
}
}
)
}
}
}
class AttachmentProviderFactory @Inject constructor(
private val imageContentRenderer: ImageContentRenderer,
private val vectorDateFormatter: VectorDateFormatter,
private val session: Session
) {
fun createProvider(attachments: List<TimelineEvent>, initialIndex: Int): RoomEventsAttachmentProvider {
return RoomEventsAttachmentProvider(attachments, initialIndex, imageContentRenderer, vectorDateFormatter, session.fileService())
}
fun createProvider(attachments: List<AttachmentData>, room: Room?, initialIndex: Int): DataAttachmentRoomProvider {
return DataAttachmentRoomProvider(attachments, room, initialIndex, imageContentRenderer, vectorDateFormatter, session.fileService())
}
}

View File

@ -0,0 +1,277 @@
/*
* 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.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.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
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 javax.inject.Inject
import kotlin.system.measureTimeMillis
class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmentProvider.InteractionListener {
@Parcelize
data class Args(
val roomId: String?,
val eventId: String,
val sharedTransitionName: String?
) : Parcelable
@Inject
lateinit var sessionHolder: ActiveSessionHolder
@Inject
lateinit var dataSourceFactory: AttachmentProviderFactory
@Inject
lateinit var imageContentRenderer: ImageContentRenderer
private lateinit var screenComponent: ScreenComponent
private var initialIndex = 0
private var isAnimatingOut = false
var currentSourceProvider: BaseAttachmentProvider? = null
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")
if (savedInstanceState == null && addTransitionListener()) {
args.sharedTransitionName?.let {
ViewCompat.setTransitionName(imageTransitionView, it)
transitionImageContainer.isVisible = true
// Postpone transaction a bit until thumbnail is loaded
val mediaData: Parcelable? = intent.getParcelableExtra(EXTRA_IMAGE_DATA)
if (mediaData is ImageContentRenderer.Data) {
// will be shown at end of transition
pager2.isInvisible = true
supportPostponeEnterTransition()
imageContentRenderer.renderThumbnailDontTransform(mediaData, imageTransitionView) {
// Proceed with transaction
scheduleStartPostponedTransition(imageTransitionView)
}
} else if (mediaData is VideoContentRenderer.Data) {
// will be shown at end of transition
pager2.isInvisible = true
supportPostponeEnterTransition()
imageContentRenderer.renderThumbnailDontTransform(mediaData.thumbnailMediaData, imageTransitionView) {
// Proceed with transaction
scheduleStartPostponedTransition(imageTransitionView)
}
}
}
}
val session = sessionHolder.getSafeActiveSession() ?: return Unit.also { finish() }
val room = args.roomId?.let { session.getRoom(it) }
val inMemoryData = intent.getParcelableArrayListExtra<AttachmentData>(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)
}
}
}
window.statusBarColor = ContextCompat.getColor(this, R.color.black_alpha)
window.navigationBarColor = ContextCompat.getColor(this, R.color.black_alpha)
}
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 = {
// The listener is also called when we are exiting
// so we use a boolean to avoid reshowing pager at end of dismiss transition
if (!isAnimatingOut) {
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<Args>(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"
const val EXTRA_IN_MEMORY_DATA = "EXTRA_IN_MEMORY_DATA"
fun newIntent(context: Context,
mediaData: AttachmentData,
roomId: String?,
eventId: String,
inMemoryData: List<AttachmentData>,
sharedTransitionName: String?) = Intent(context, VectorAttachmentViewerActivity::class.java).also {
it.putExtra(EXTRA_ARGS, Args(roomId, eventId, sharedTransitionName))
it.putExtra(EXTRA_IMAGE_DATA, mediaData)
if (inMemoryData.isNotEmpty()) {
it.putParcelableArrayListExtra(EXTRA_IN_MEMORY_DATA, ArrayList(inMemoryData))
}
}
}
override fun onDismissTapped() {
animateClose()
}
override fun onPlayPause(play: Boolean) {
handle(if (play) AttachmentCommands.StartVideo else AttachmentCommands.PauseVideo)
}
override fun videoSeekTo(percent: Int) {
handle(AttachmentCommands.SeekTo(percent))
}
override fun onShareTapped() {
this.currentSourceProvider?.getFileForSharing(currentPosition) { data ->
if (data != null && lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
shareMedia(this@VectorAttachmentViewerActivity, data, getMimeTypeFromUri(this@VectorAttachmentViewerActivity, data.toUri()))
}
}
}
}

View File

@ -16,7 +16,6 @@
package im.vector.riotx.features.media package im.vector.riotx.features.media
import android.os.Parcelable
import android.widget.ImageView import android.widget.ImageView
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
@ -38,13 +37,13 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder:
@Parcelize @Parcelize
data class Data( data class Data(
val eventId: String, override val eventId: String,
val filename: String, override val filename: String,
val mimeType: String?, override val mimeType: String?,
val url: String?, override val url: String?,
val elementToDecrypt: ElementToDecrypt?, override val elementToDecrypt: ElementToDecrypt?,
val thumbnailMediaData: ImageContentRenderer.Data val thumbnailMediaData: ImageContentRenderer.Data
) : Parcelable ) : AttachmentData
fun render(data: Data, fun render(data: Data,
thumbnailView: ImageView, thumbnailView: ImageView,
@ -70,7 +69,7 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder:
downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE, downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
id = data.eventId, id = data.eventId,
fileName = data.filename, fileName = data.filename,
mimeType = null, mimeType = data.mimeType,
url = data.url, url = data.url,
elementToDecrypt = data.elementToDecrypt, elementToDecrypt = data.elementToDecrypt,
callback = object : MatrixCallback<File> { callback = object : MatrixCallback<File> {

View File

@ -79,13 +79,13 @@ class VideoMediaViewerActivity : VectorBaseActivity() {
private fun onShareActionClicked() { private fun onShareActionClicked() {
session.fileService().downloadFile( session.fileService().downloadFile(
FileService.DownloadMode.FOR_EXTERNAL_SHARE, downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
mediaData.eventId, id = mediaData.eventId,
mediaData.filename, fileName = mediaData.filename,
mediaData.mimeType, mimeType = mediaData.mimeType,
mediaData.url, url = mediaData.url,
mediaData.elementToDecrypt, elementToDecrypt = mediaData.elementToDecrypt,
object : MatrixCallback<File> { callback = object : MatrixCallback<File> {
override fun onSuccess(data: File) { override fun onSuccess(data: File) {
shareMedia(this@VideoMediaViewerActivity, data, getMimeTypeFromUri(this@VideoMediaViewerActivity, data.toUri())) shareMedia(this@VideoMediaViewerActivity, data, getMimeTypeFromUri(this@VideoMediaViewerActivity, data.toUri()))
} }

View File

@ -19,7 +19,6 @@ package im.vector.riotx.features.navigation
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.view.View import android.view.View
import android.view.Window import android.view.Window
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
@ -50,11 +49,9 @@ 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.detail.widget.WidgetRequestCodes
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
import im.vector.riotx.features.invite.InviteUsersToRoomActivity 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.BigImageViewerActivity
import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VectorAttachmentViewerActivity
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.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity
@ -90,7 +87,8 @@ class DefaultNavigator @Inject constructor(
override fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) { override fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) {
val session = sessionHolder.getSafeActiveSession() ?: return 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() (tx as? IncomingSasVerificationTransaction)?.performAccept()
if (context is VectorBaseActivity) { if (context is VectorBaseActivity) {
VerificationBottomSheet.withArgs( VerificationBottomSheet.withArgs(
@ -117,6 +115,27 @@ 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 }
if (context is VectorBaseActivity) {
if (otherSessions.isNotEmpty()) {
val pr = session.cryptoService().verificationService().requestKeyVerification(
supportedVerificationMethodsProvider.provide(),
session.myUserId,
otherSessions)
VerificationBottomSheet.forSelfVerification(session, pr.transactionId ?: pr.localId)
.show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG)
} else {
VerificationBottomSheet.forSelfVerification(session)
.show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG)
}
}
}
override fun waitSessionVerification(context: Context) { override fun waitSessionVerification(context: Context) {
val session = sessionHolder.getSafeActiveSession() ?: return val session = sessionHolder.getSafeActiveSession() ?: return
if (context is VectorBaseActivity) { if (context is VectorBaseActivity) {
@ -200,7 +219,14 @@ class DefaultNavigator @Inject constructor(
} }
override fun openKeysBackupSetup(context: Context, showManualExport: Boolean) { 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) { override fun openKeysBackupManager(context: Context) {
@ -217,7 +243,8 @@ class DefaultNavigator @Inject constructor(
?.let { avatarUrl -> ?.let { avatarUrl ->
val intent = BigImageViewerActivity.newIntent(activity, matrixItem.getBestName(), avatarUrl) val intent = BigImageViewerActivity.newIntent(activity, matrixItem.getBestName(), avatarUrl)
val options = sharedElement?.let { val options = sharedElement?.let {
ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it) ?: "") ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it)
?: "")
} }
activity.startActivity(intent, options?.toBundle()) activity.startActivity(intent, options?.toBundle())
} }
@ -245,27 +272,32 @@ class DefaultNavigator @Inject constructor(
context.startActivity(WidgetActivity.newIntent(context, widgetArgs)) context.startActivity(WidgetActivity.newIntent(context, widgetArgs))
} }
override fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList<Pair<View, String>>) -> Unit)?) { override fun openMediaViewer(activity: Activity,
val intent = ImageMediaViewerActivity.newIntent(activity, mediaData, ViewCompat.getTransitionName(view)) roomId: String,
val pairs = ArrayList<Pair<View, String>>() mediaData: AttachmentData,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { view: View,
inMemory: List<AttachmentData>,
options: ((MutableList<Pair<View, String>>) -> Unit)?) {
VectorAttachmentViewerActivity.newIntent(activity,
mediaData,
roomId,
mediaData.eventId,
inMemory,
ViewCompat.getTransitionName(view)).let { intent ->
val pairs = ArrayList<Pair<View, String>>()
activity.window.decorView.findViewById<View>(android.R.id.statusBarBackground)?.let { activity.window.decorView.findViewById<View>(android.R.id.statusBarBackground)?.let {
pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME))
} }
activity.window.decorView.findViewById<View>(android.R.id.navigationBarBackground)?.let { activity.window.decorView.findViewById<View>(android.R.id.navigationBarBackground)?.let {
pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) 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)
} }
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) {
val intent = VideoMediaViewerActivity.newIntent(activity, mediaData)
activity.startActivity(intent)
} }
private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) { private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) {

View File

@ -24,11 +24,10 @@ import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
import im.vector.matrix.android.api.session.terms.TermsService 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.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.home.room.detail.widget.WidgetRequestCodes
import im.vector.riotx.features.media.ImageContentRenderer 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.settings.VectorSettingsActivity
import im.vector.riotx.features.share.SharedData import im.vector.riotx.features.share.SharedData
import im.vector.riotx.features.terms.ReviewTermsActivity import im.vector.riotx.features.terms.ReviewTermsActivity
@ -41,6 +40,8 @@ interface Navigator {
fun requestSessionVerification(context: Context, otherSessionId: String) fun requestSessionVerification(context: Context, otherSessionId: String)
fun requestSelfSessionVerification(context: Context)
fun waitSessionVerification(context: Context) fun waitSessionVerification(context: Context)
fun upgradeSessionSecurity(context: Context, initCrossSigningOnly: Boolean) fun upgradeSessionSecurity(context: Context, initCrossSigningOnly: Boolean)
@ -92,7 +93,10 @@ interface Navigator {
fun openRoomWidget(context: Context, roomId: String, widget: Widget) fun openRoomWidget(context: Context, roomId: String, widget: Widget)
fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList<Pair<View, String>>) -> Unit)?) fun openMediaViewer(activity: Activity,
roomId: String,
fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data) mediaData: AttachmentData,
view: View,
inMemory: List<AttachmentData> = emptyList(),
options: ((MutableList<Pair<View, String>>) -> Unit)?)
} }

View File

@ -22,10 +22,11 @@ import android.os.HandlerThread
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.Person 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.matrix.android.api.session.content.ContentUrlResolver
import im.vector.riotx.ActiveSessionDataSource
import im.vector.riotx.BuildConfig import im.vector.riotx.BuildConfig
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorPreferences
import me.gujun.android.span.span import me.gujun.android.span.span
@ -46,7 +47,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
private val notificationUtils: NotificationUtils, private val notificationUtils: NotificationUtils,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionDataSource: ActiveSessionDataSource,
private val iconLoader: IconLoader, private val iconLoader: IconLoader,
private val bitmapLoader: BitmapLoader, private val bitmapLoader: BitmapLoader,
private val outdatedDetector: OutdatedEventDetector?) { private val outdatedDetector: OutdatedEventDetector?) {
@ -68,6 +69,10 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
private var currentRoomId: String? = null 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. 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 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() { private fun refreshNotificationDrawerBg() {
Timber.v("refreshNotificationDrawerBg()") Timber.v("refreshNotificationDrawerBg()")
val session = activeSessionHolder.getSafeActiveSession() ?: return val session = currentSession ?: return
val user = session.getUser(session.myUserId) val user = session.getUser(session.myUserId)
// myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash // 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) val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
if (!file.exists()) file.createNewFile() if (!file.exists()) file.createNewFile()
FileOutputStream(file).use { FileOutputStream(file).use {
activeSessionHolder.getSafeActiveSession()?.securelyStoreObject(eventList, KEY_ALIAS_SECRET_STORAGE, it) currentSession?.securelyStoreObject(eventList, KEY_ALIAS_SECRET_STORAGE, it)
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Timber.e(e, "## Failed to save cached notification info") 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) val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
if (file.exists()) { if (file.exists()) {
FileInputStream(file).use { FileInputStream(file).use {
val events: ArrayList<NotifiableEvent>? = activeSessionHolder.getSafeActiveSession()?.loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE) val events: ArrayList<NotifiableEvent>? = currentSession?.loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE)
if (events != null) { if (events != null) {
return events.toMutableList() return events.toMutableList()
} }

View File

@ -15,10 +15,12 @@
*/ */
package im.vector.riotx.features.notifications package im.vector.riotx.features.notifications
import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.ActiveSessionDataSource
import javax.inject.Inject 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. * Returns true if the given event is outdated.
@ -26,10 +28,12 @@ class OutdatedEventDetector @Inject constructor(private val activeSessionHolder:
* other device. * other device.
*/ */
fun isMessageOutdated(notifiableEvent: NotifiableEvent): Boolean { fun isMessageOutdated(notifiableEvent: NotifiableEvent): Boolean {
val session = activeSessionDataSource.currentValue?.orNull() ?: return false
if (notifiableEvent is NotifiableMessageEvent) { if (notifiableEvent is NotifiableMessageEvent) {
val eventID = notifiableEvent.eventId val eventID = notifiableEvent.eventId
val roomID = notifiableEvent.roomId val roomID = notifiableEvent.roomId
val room = activeSessionHolder.getSafeActiveSession()?.getRoom(roomID) ?: return false val room = session.getRoom(roomID) ?: return false
return room.isEventRead(eventID) return room.isEventRead(eventID)
} }
return false return false

View File

@ -30,17 +30,17 @@ class PushRuleTriggerListener @Inject constructor(
private val notificationDrawerManager: NotificationDrawerManager private val notificationDrawerManager: NotificationDrawerManager
) : PushRuleService.PushRuleListener { ) : PushRuleService.PushRuleListener {
var session: Session? = null private var session: Session? = null
override fun onMatchRule(event: Event, actions: List<Action>) { override fun onMatchRule(event: Event, actions: List<Action>) {
Timber.v("Push rule match for event ${event.eventId}") Timber.v("Push rule match for event ${event.eventId}")
if (session == null) { val safeSession = session ?: return Unit.also {
Timber.e("Called without active session") Timber.e("Called without active session")
return
} }
val notificationAction = actions.toNotificationAction() val notificationAction = actions.toNotificationAction()
if (notificationAction.shouldNotify) { if (notificationAction.shouldNotify) {
val notifiableEvent = resolver.resolveEvent(event, session!!) val notifiableEvent = resolver.resolveEvent(event, safeSession)
if (notifiableEvent == null) { if (notifiableEvent == null) {
Timber.v("## Failed to resolve event") Timber.v("## Failed to resolve event")
// TODO // TODO

View File

@ -26,6 +26,7 @@ import com.tapadoo.alerter.Alerter
import com.tapadoo.alerter.OnHideAlertListener import com.tapadoo.alerter.OnHideAlertListener
import dagger.Lazy import dagger.Lazy
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.themes.ThemeUtils
import timber.log.Timber import timber.log.Timber
@ -83,7 +84,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava
setLightStatusBar() setLightStatusBar()
} }
} }
if (currentAlerter?.shouldBeDisplayedIn?.invoke(activity) == false) { if (currentAlerter?.shouldBeDisplayedIn?.invoke(activity) == false || activity !is VectorBaseActivity) {
return return
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright 2020 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -34,6 +34,7 @@ import android.widget.TextView
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.withStyledAttributes
import im.vector.riotx.EmojiCompatWrapper import im.vector.riotx.EmojiCompatWrapper
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.di.HasScreenInjector
@ -44,7 +45,8 @@ import javax.inject.Inject
* An animated reaction button. * An animated reaction button.
* Displays a String reaction (emoji), with a count, and that can be selected or not (toggle) * 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) defStyleAttr: Int = 0)
: FrameLayout(context, attrs, defStyleAttr), View.OnClickListener, View.OnLongClickListener { : FrameLayout(context, attrs, defStyleAttr), View.OnClickListener, View.OnLongClickListener {
@ -109,42 +111,41 @@ class ReactionButton @JvmOverloads constructor(context: Context, attrs: Attribut
countTextView?.text = TextUtils.formatCountToShortDecimal(reactionCount) countTextView?.text = TextUtils.formatCountToShortDecimal(reactionCount)
// emojiView?.typeface = this.emojiTypeFace ?: Typeface.DEFAULT // 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)
val array = context.obtainStyledAttributes(attrs, R.styleable.ReactionButton, defStyleAttr, 0) circleStartColor = getColor(R.styleable.ReactionButton_circle_start_color, 0)
onDrawable = ContextCompat.getDrawable(context, R.drawable.rounded_rect_shape) if (circleStartColor != 0) {
offDrawable = ContextCompat.getDrawable(context, R.drawable.rounded_rect_shape_off) circleView.startColor = circleStartColor
}
circleStartColor = array.getColor(R.styleable.ReactionButton_circle_start_color, 0) circleEndColor = getColor(R.styleable.ReactionButton_circle_end_color, 0)
if (circleStartColor != 0) { if (circleEndColor != 0) {
circleView.startColor = circleStartColor 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)
} }
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) setOnClickListener(this)
setOnLongClickListener(this) setOnLongClickListener(this)
array.recycle()
} }
private fun getDrawableFromResource(array: TypedArray, styleableIndexId: Int): Drawable? { private fun getDrawableFromResource(array: TypedArray, styleableIndexId: Int): Drawable? {

View File

@ -158,13 +158,13 @@ class RoomUploadsViewModel @AssistedInject constructor(
try { try {
val file = awaitCallback<File> { val file = awaitCallback<File> {
session.fileService().downloadFile( session.fileService().downloadFile(
FileService.DownloadMode.FOR_EXTERNAL_SHARE, downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
action.uploadEvent.eventId, id = action.uploadEvent.eventId,
action.uploadEvent.contentWithAttachmentContent.body, fileName = action.uploadEvent.contentWithAttachmentContent.body,
action.uploadEvent.contentWithAttachmentContent.getFileUrl(), mimeType = action.uploadEvent.contentWithAttachmentContent.mimeType,
action.uploadEvent.contentWithAttachmentContent.mimeType, url = action.uploadEvent.contentWithAttachmentContent.getFileUrl(),
action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(), elementToDecrypt = action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(),
it) callback = it)
} }
_viewEvents.post(RoomUploadsViewEvents.FileReadyForSaving(file, action.uploadEvent.contentWithAttachmentContent.body)) _viewEvents.post(RoomUploadsViewEvents.FileReadyForSaving(file, action.uploadEvent.contentWithAttachmentContent.body))
} catch (failure: Throwable) { } catch (failure: Throwable) {

View File

@ -20,23 +20,34 @@ import android.os.Bundle
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.View import android.view.View
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState 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.R
import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.trackItemsVisibilityChange import im.vector.riotx.core.extensions.trackItemsVisibilityChange
import im.vector.riotx.core.platform.StateView import im.vector.riotx.core.platform.StateView
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.DimensionConverter 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.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer import im.vector.riotx.features.media.VideoContentRenderer
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsAction 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.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_generic_state_view_recycler.*
import kotlinx.android.synthetic.main.fragment_room_uploads.*
import javax.inject.Inject import javax.inject.Inject
class RoomUploadsMediaFragment @Inject constructor( class RoomUploadsMediaFragment @Inject constructor(
@ -76,12 +87,86 @@ class RoomUploadsMediaFragment @Inject constructor(
controller.listener = null controller.listener = null
} }
override fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) { // It's very strange i can't just access
navigator.openImageViewer(requireActivity(), mediaData, view, null) // the app bar using find by id...
private fun trickFindAppBar(): AppBarLayout? {
return activity?.supportFragmentManager?.fragments
?.filterIsInstance<RoomUploadsFragment>()
?.firstOrNull()
?.roomUploadsAppBar
} }
override fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) { override fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) = withState(uploadsViewModel) { state ->
navigator.openVideoViewer(requireActivity(), mediaData) val inMemory = getItemsArgs(state)
navigator.openMediaViewer(
activity = requireActivity(),
roomId = state.roomId,
mediaData = mediaData,
view = view,
inMemory = inMemory
) { pairs ->
trickFindAppBar()?.let {
pairs.add(Pair(it, ViewCompat.getTransitionName(it) ?: ""))
}
}
}
private fun getItemsArgs(state: RoomUploadsViewState): List<AttachmentData> {
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.openMediaViewer(
activity = requireActivity(),
roomId = state.roomId,
mediaData = mediaData,
view = view,
inMemory = inMemory
) { pairs ->
trickFindAppBar()?.let {
pairs.add(Pair(it, ViewCompat.getTransitionName(it) ?: ""))
}
}
} }
override fun loadMore() { override fun loadMore() {

View File

@ -18,11 +18,13 @@ package im.vector.riotx.features.roomprofile.uploads.media
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import androidx.core.view.ViewCompat
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel 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.ImageContentRenderer
@EpoxyModelClass(layout = R.layout.item_uploads_image) @EpoxyModelClass(layout = R.layout.item_uploads_image)
@ -35,8 +37,13 @@ abstract class UploadsImageItem : VectorEpoxyModel<UploadsImageItem.Holder>() {
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(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) imageContentRenderer.render(data, holder.imageView, IMAGE_SIZE_DP)
ViewCompat.setTransitionName(holder.imageView, "imagePreview_${id()}")
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {

View File

@ -18,11 +18,13 @@ package im.vector.riotx.features.roomprofile.uploads.media
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import androidx.core.view.ViewCompat
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel 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.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer import im.vector.riotx.features.media.VideoContentRenderer
@ -36,8 +38,13 @@ abstract class UploadsVideoItem : VectorEpoxyModel<UploadsVideoItem.Holder>() {
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(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) imageContentRenderer.render(data.thumbnailMediaData, holder.imageView, IMAGE_SIZE_DP)
ViewCompat.setTransitionName(holder.imageView, "videoPreview_${id()}")
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {

View File

@ -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.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.dialogs.ExportKeysDialog 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.ExternalIntentData
import im.vector.riotx.core.intent.analyseIntent import im.vector.riotx.core.intent.analyseIntent
import im.vector.riotx.core.intent.getFilenameFromUri import im.vector.riotx.core.intent.getFilenameFromUri
import im.vector.riotx.core.platform.SimpleTextWatcher import im.vector.riotx.core.platform.SimpleTextWatcher
import im.vector.riotx.core.preference.VectorPreference 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.PERMISSION_REQUEST_CODE_EXPORT_KEYS
import im.vector.riotx.core.utils.allGranted 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.openFileSelection
import im.vector.riotx.core.utils.toast import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.crypto.keys.KeysExporter 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 import javax.inject.Inject
class VectorSettingsSecurityPrivacyFragment @Inject constructor( class VectorSettingsSecurityPrivacyFragment @Inject constructor(
private val vectorPreferences: VectorPreferences private val vectorPreferences: VectorPreferences,
private val activeSessionHolder: ActiveSessionHolder
) : VectorSettingsBaseFragment() { ) : VectorSettingsBaseFragment() {
override var titleRes = R.string.settings_security_and_privacy override var titleRes = R.string.settings_security_and_privacy
@ -119,38 +120,69 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
} }
private fun refreshXSigningStatus() { private fun refreshXSigningStatus() {
val xSigningIsEnableInAccount = session.cryptoService().crossSigningService().isCrossSigningInitialized() val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified() val xSigningIsEnableInAccount = crossSigningKeys != null
val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign() val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified()
val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign()
if (xSigningKeyCanSign) { if (xSigningKeyCanSign) {
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted) mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete) mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete)
} else if (xSigningKeysAreTrusted) { } else if (xSigningKeysAreTrusted) {
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom) mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted) mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted)
} else if (xSigningIsEnableInAccount) { } else if (xSigningIsEnableInAccount) {
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black) mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted) mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted)
} else { } else {
mCrossSigningStatePreference.setIcon(android.R.color.transparent) mCrossSigningStatePreference.setIcon(android.R.color.transparent)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_disabled) mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_disabled)
} }
mCrossSigningStatePreference.isVisible = true mCrossSigningStatePreference.isVisible = true
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
if (allGranted(grantResults)) { if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) { 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?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) 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<Boolean> {
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) { if (resultCode == Activity.RESULT_OK) {
when (requestCode) { when (requestCode) {
REQUEST_E2E_FILE_REQUEST_CODE -> importKeys(data) REQUEST_E2E_FILE_REQUEST_CODE -> importKeys(data)
@ -169,7 +201,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
} }
exportPref.onPreferenceClickListener = Preference.OnPreferenceClickListener { exportPref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
exportKeys() queryExportKeys(activeSessionHolder.getSafeActiveSession()?.myUserId ?: "", REQUEST_CODE_SAVE_MEGOLM_EXPORT)
true 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<String> {
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. * Manage the e2e keys import.
*/ */
@ -515,6 +507,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
companion object { companion object {
private const val REQUEST_E2E_FILE_REQUEST_CODE = 123 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 PUSHER_PREFERENCE_KEY_BASE = "PUSHER_PREFERENCE_KEY_BASE"
private const val DEVICES_PREFERENCE_KEY_BASE = "DEVICES_PREFERENCE_KEY_BASE" private const val DEVICES_PREFERENCE_KEY_BASE = "DEVICES_PREFERENCE_KEY_BASE"

View File

@ -51,7 +51,7 @@ class CrossSigningSettingsFragment @Inject constructor(
Unit Unit
} }
CrossSigningSettingsViewEvents.VerifySession -> { CrossSigningSettingsViewEvents.VerifySession -> {
navigator.waitSessionVerification(requireActivity()) navigator.requestSelfSessionVerification(requireActivity())
} }
CrossSigningSettingsViewEvents.SetUpRecovery -> { CrossSigningSettingsViewEvents.SetUpRecovery -> {
navigator.upgradeSessionSecurity(requireActivity(), false) navigator.upgradeSessionSecurity(requireActivity(), false)

View File

@ -38,4 +38,10 @@ sealed class ActivityOtherThemes(@StyleRes val dark: Int,
R.style.AppTheme_AttachmentsPreview, R.style.AppTheme_AttachmentsPreview,
R.style.AppTheme_AttachmentsPreview R.style.AppTheme_AttachmentsPreview
) )
object VectorAttachmentsPreview : ActivityOtherThemes(
R.style.AppTheme_Transparent,
R.style.AppTheme_Transparent,
R.style.AppTheme_Transparent
)
} }

View File

@ -0,0 +1,177 @@
/*
* 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<BannerState> = 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<ServerBackupStatusViewState, EmptyAction, EmptyViewEvents>(initialState), KeysBackupStateListener {
@AssistedInject.Factory
interface Factory {
fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel
}
companion object : MvRxViewModelFactory<ServerBackupStatusViewModel, ServerBackupStatusViewState> {
@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<Boolean>()
val keysBackupState = MutableLiveData<KeysBackupState>()
private val keyBackupPublishSubject: PublishSubject<KeysBackupState> = PublishSubject.create()
init {
session.cryptoService().keysBackupService().addListener(this)
keysBackupState.value = session.cryptoService().keysBackupService().state
Observable.combineLatest<List<UserAccountData>, Optional<MXCrossSigningInfo>, KeysBackupState, Optional<PrivateKeysInfo>, 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(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)
}
/**
* 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) {}
}

View File

@ -28,19 +28,27 @@ import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.transition.TransitionManager
import butterknife.BindView 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.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog 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.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.riotx.R 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.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity
import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet
import timber.log.Timber
import javax.inject.Inject
class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() { // 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) @BindView(R.id.bottom_sheet_signout_warning_text)
lateinit var sheetTitle: TextView lateinit var sheetTitle: TextView
@ -48,14 +56,20 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() {
@BindView(R.id.bottom_sheet_signout_backingup_status_group) @BindView(R.id.bottom_sheet_signout_backingup_status_group)
lateinit var backingUpStatusGroup: ViewGroup lateinit var backingUpStatusGroup: ViewGroup
@BindView(R.id.keys_backup_setup) @BindView(R.id.setupRecoveryButton)
lateinit var setupClickableView: View lateinit var setupRecoveryButton: SignoutBottomSheetActionButton
@BindView(R.id.keys_backup_activate) @BindView(R.id.setupMegolmBackupButton)
lateinit var activateClickableView: View lateinit var setupMegolmBackupButton: SignoutBottomSheetActionButton
@BindView(R.id.keys_backup_dont_want) @BindView(R.id.exportManuallyButton)
lateinit var dontWantClickableView: View 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) @BindView(R.id.bottom_sheet_signout_icon_progress_bar)
lateinit var backupProgress: ProgressBar lateinit var backupProgress: ProgressBar
@ -66,8 +80,8 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() {
@BindView(R.id.bottom_sheet_backup_status_text) @BindView(R.id.bottom_sheet_backup_status_text)
lateinit var backupStatusTex: TextView lateinit var backupStatusTex: TextView
@BindView(R.id.bottom_sheet_signout_button) @BindView(R.id.signoutExportingLoading)
lateinit var signoutClickableView: View lateinit var signoutExportingLoading: View
@BindView(R.id.root_layout) @BindView(R.id.root_layout)
lateinit var rootLayout: ViewGroup lateinit var rootLayout: ViewGroup
@ -78,62 +92,44 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() {
fun newInstance() = SignOutBottomSheetDialogFragment() fun newInstance() = SignOutBottomSheetDialogFragment()
private const val EXPORT_REQ = 0 private const val EXPORT_REQ = 0
private const val QUERY_EXPORT_KEYS = 1
} }
init { init {
isCancelable = true isCancelable = true
} }
private lateinit var viewModel: SignOutViewModel @Inject
lateinit var viewModelFactory: SignoutCheckViewModel.Factory
override fun create(initialState: SignoutCheckViewState): SignoutCheckViewModel {
return viewModelFactory.create(initialState)
}
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?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
viewModel = fragmentViewModelProvider.get(SignOutViewModel::class.java) setupRecoveryButton.action = {
BootstrapBottomSheet.show(parentFragmentManager, false)
setupClickableView.setOnClickListener {
context?.let { context ->
startActivityForResult(KeysBackupSetupActivity.intent(context, true), EXPORT_REQ)
}
} }
activateClickableView.setOnClickListener { exitAnywayButton.action = {
context?.let { context ->
startActivity(KeysBackupManageActivity.intent(context))
}
}
signoutClickableView.setOnClickListener {
this.onSignOut?.run()
}
dontWantClickableView.setOnClickListener { _ ->
context?.let { context?.let {
AlertDialog.Builder(it) AlertDialog.Builder(it)
.setTitle(R.string.are_you_sure) .setTitle(R.string.are_you_sure)
.setMessage(R.string.sign_out_bottom_sheet_will_lose_secure_messages) .setMessage(R.string.sign_out_bottom_sheet_will_lose_secure_messages)
.setPositiveButton(R.string.backup) { _, _ -> .setPositiveButton(R.string.backup, null)
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
}
}
}
.setNegativeButton(R.string.action_sign_out) { _, _ -> .setNegativeButton(R.string.action_sign_out) { _, _ ->
onSignOut?.run() onSignOut?.run()
} }
@ -141,71 +137,143 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() {
} }
} }
viewModel.keysExportedToFile.observe(viewLifecycleOwner, Observer { exportManuallyButton.action = {
val hasExportedToFile = it ?: false withState(viewModel) { state ->
if (hasExportedToFile) { queryExportKeys(state.userId, QUERY_EXPORT_KEYS)
// 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
} }
}) }
viewModel.keysBackupState.observe(viewLifecycleOwner, Observer { setupMegolmBackupButton.action = {
if (viewModel.keysExportedToFile.value == true) { startActivityForResult(KeysBackupSetupActivity.intent(requireContext(), true), EXPORT_REQ)
// ignore this }
return@Observer
} viewModel.observeViewEvents {
TransitionManager.beginDelayedTransition(rootLayout)
when (it) { when (it) {
KeysBackupState.ReadyToBackUp -> { is SignoutCheckViewModel.ViewEvents.ExportKeys -> {
signoutClickableView.isVisible = true it.exporter
dontWantClickableView.isVisible = false .export(requireContext(),
setupClickableView.isVisible = false it.passphrase,
activateClickableView.isVisible = false it.uri,
backingUpStatusGroup.isVisible = true object : MatrixCallback<Boolean> {
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 backupProgress.isVisible = false
backupCompleteImage.isVisible = true backupCompleteImage.isVisible = true
backupStatusTex.text = getString(R.string.keys_backup_info_keys_all_backup_up) 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 backupProgress.isVisible = true
backupCompleteImage.isVisible = false backupCompleteImage.isVisible = false
backupStatusTex.text = getString(R.string.sign_out_bottom_sheet_backing_up_keys) backupStatusTex.text = getString(R.string.sign_out_bottom_sheet_backing_up_keys)
hideViews(setupMegolmBackupButton, setupMegolmBackupButton, signOutButton, exportManuallyButton)
exitAnywayButton.isVisible = true
} }
KeysBackupState.NotTrusted -> { 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) 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 -> { else -> {
backingUpStatusGroup.isVisible = false // mmm.. strange state
dontWantClickableView.isVisible = true
setupClickableView.isVisible = true exitAnywayButton.isVisible = true
activateClickableView.isVisible = false
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_no_backup)
} }
} }
}
// 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 override fun getLayoutResId() = R.layout.bottom_sheet_logout_and_backup
@ -228,10 +296,26 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment() {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
if (requestCode == EXPORT_REQ) { if (requestCode == QUERY_EXPORT_KEYS) {
val manualExportDone = data?.getBooleanExtra(KeysBackupSetupActivity.MANUAL_EXPORT, false) val uri = data?.data
viewModel.keysExportedToFile.value = manualExportDone 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 }
}
} }

View File

@ -21,7 +21,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder 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.core.extensions.vectorComponent
import im.vector.riotx.features.MainActivity import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.MainActivityArgs import im.vector.riotx.features.MainActivityArgs
@ -33,7 +33,7 @@ class SignOutUiWorker(private val activity: FragmentActivity) {
fun perform(context: Context) { fun perform(context: Context) {
activeSessionHolder = context.vectorComponent().activeSessionHolder() activeSessionHolder = context.vectorComponent().activeSessionHolder()
val session = activeSessionHolder.getActiveSession() 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 // 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() val signOutDialog = SignOutBottomSheetDialogFragment.newInstance()
signOutDialog.onSignOut = Runnable { signOutDialog.onSignOut = Runnable {

View File

@ -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<Boolean>()
var keysBackupState = MutableLiveData<KeysBackupState>()
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()
}
}
}

View File

@ -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()
}
}
}

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