Merge branch 'develop' into feature/fix_small_issues
This commit is contained in:
commit
3fc9fe3017
@ -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
1
attachment-viewer/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
78
attachment-viewer/build.gradle
Normal file
78
attachment-viewer/build.gradle
Normal 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'
|
||||||
|
|
||||||
|
}
|
0
attachment-viewer/consumer-rules.pro
Normal file
0
attachment-viewer/consumer-rules.pro
Normal file
21
attachment-viewer/proguard-rules.pro
vendored
Normal file
21
attachment-viewer/proguard-rules.pro
vendored
Normal 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
|
2
attachment-viewer/src/main/AndroidManifest.xml
Normal file
2
attachment-viewer/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="im.vector.riotx.attachmentviewer" />
|
@ -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)
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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) }
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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 {
|
||||||
|
@ -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/
|
||||||
|
|
||||||
|
|
||||||
╔════════════════════════════════════════════════╗
|
╔════════════════════════════════════════════════╗
|
||||||
|
@ -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
|
||||||
) {
|
) {
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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>)
|
||||||
|
|
||||||
|
@ -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>
|
||||||
}
|
}
|
||||||
|
@ -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 ")
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
|
@ -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'
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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) {
|
||||||
|
@ -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 {
|
||||||
|
47
vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt
Normal file
47
vector/src/main/java/im/vector/riotx/core/di/ImageManager.kt
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
|
@ -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.
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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))
|
||||||
|
@ -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()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()))
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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))
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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()))
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
@ -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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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> {
|
||||||
|
@ -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()))
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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)?)
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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? {
|
||||||
|
@ -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) {
|
||||||
|
@ -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() {
|
||||||
|
@ -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() {
|
||||||
|
@ -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() {
|
||||||
|
@ -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"
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -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) {}
|
||||||
|
}
|
@ -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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
Loading…
x
Reference in New Issue
Block a user