mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 18:35:40 +03:00
Merge branch 'release/0.91.5'
This commit is contained in:
commit
b9f0c176d9
337 changed files with 8536 additions and 3322 deletions
42
CHANGES.md
42
CHANGES.md
|
@ -1,4 +1,42 @@
|
|||
Changes in Riot.imX 0.91.4 (2020-XX-XX)
|
||||
Changes in Riot.imX 0.91.5 (2020-07-11)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
- 3pid invite: it is now possible to invite people by email. An Identity Server has to be configured (#548)
|
||||
|
||||
Improvements 🙌:
|
||||
- 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)
|
||||
- Handling (almost) properly the groups fetching (#1634)
|
||||
- Improve fullscreen media display (#327)
|
||||
- Setup server recovery banner (#1648)
|
||||
- Set up SSSS from security settings (#1567)
|
||||
- New lab setting to add 'unread notifications' tab to main screen
|
||||
- Render third party invite event (#548)
|
||||
- Display three pid invites in the room members list (#548)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Integration Manager: Wrong URL to review terms if URL in config contains path (#1606)
|
||||
- Regression Composer does not grow, crops out text (#1650)
|
||||
- Bug / Unwanted draft (#698)
|
||||
- All users seems to be able to see the enable encryption option in room settings (#1341)
|
||||
- Leave room only leaves the current version (#1656)
|
||||
- Regression | Share action menu do not work (#1647)
|
||||
- verification issues on transition (#1555)
|
||||
- Fix issue when restoring keys backup using recovery key
|
||||
|
||||
SDK API changes ⚠️:
|
||||
- CreateRoomParams has been updated
|
||||
|
||||
Build 🧱:
|
||||
- Upgrade some dependencies
|
||||
- Revert to build-tools 3.5.3
|
||||
|
||||
Other changes:
|
||||
- Use Intent.ACTION_CREATE_DOCUMENT to save megolm key or recovery key in a txt file
|
||||
- Use `Context#withStyledAttributes` extension function (#1546)
|
||||
|
||||
Changes in Riot.imX 0.91.4 (2020-07-06)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
|
@ -16,7 +54,7 @@ Bugfix 🐛:
|
|||
|
||||
Build 🧱:
|
||||
- Fix lint false-positive about WorkManager (#1012)
|
||||
- Upgrade build-tools from 3.5.3 to 3.6.6
|
||||
- Upgrade build-tools from 3.5.3 to 3.6.3
|
||||
- Upgrade gradle from 5.4.1 to 5.6.4
|
||||
|
||||
Changes in Riot.imX 0.91.3 (2020-07-01)
|
||||
|
|
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>
|
|
@ -1,7 +1,7 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.50'
|
||||
ext.kotlin_version = '1.3.72'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
|
@ -10,12 +10,13 @@ buildscript {
|
|||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.6.3'
|
||||
// Warning: 3.6.3 leads to infinite gradle builds. Stick to 3.5.3 for the moment
|
||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||
classpath 'com.google.gms:google-services:4.3.2'
|
||||
classpath "com.airbnb.okreplay:gradle-plugin:1.5.0"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1'
|
||||
classpath 'com.google.android.gms:oss-licenses-plugin:0.9.5'
|
||||
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.2'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
@ -38,6 +39,8 @@ allprojects {
|
|||
includeGroupByRegex "com\\.github\\.yalantis"
|
||||
// JsonViewer
|
||||
includeGroupByRegex 'com\\.github\\.BillCarsonFr'
|
||||
// PhotoView
|
||||
includeGroupByRegex 'com\\.github\\.chrisbanes'
|
||||
}
|
||||
}
|
||||
maven {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
Useful links:
|
||||
- https://codelabs.developers.google.com/codelabs/webrtc-web/#0
|
||||
- http://webrtc.github.io/webrtc-org/native-code/android/
|
||||
|
||||
|
||||
╔════════════════════════════════════════════════╗
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
# The setting is particularly useful for tweaking memory settings.
|
||||
android.enableJetifier=true
|
||||
android.useAndroidX=true
|
||||
org.gradle.jvmargs=-Xmx8192m
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
|
|
|
@ -39,7 +39,7 @@ dependencies {
|
|||
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
// Paging
|
||||
implementation "androidx.paging:paging-runtime-ktx:2.1.0"
|
||||
implementation "androidx.paging:paging-runtime-ktx:2.1.2"
|
||||
|
||||
// Logging
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.matrix.rx
|
|||
import android.net.Uri
|
||||
import im.vector.matrix.android.api.query.QueryStringValue
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||
import im.vector.matrix.android.api.session.room.Room
|
||||
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
|
||||
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||
|
@ -71,6 +72,13 @@ class RxRoom(private val room: Room) {
|
|||
}
|
||||
}
|
||||
|
||||
fun liveStateEvents(eventTypes: Set<String>): Observable<List<Event>> {
|
||||
return room.getStateEventsLive(eventTypes).asObservable()
|
||||
.startWithCallable {
|
||||
room.getStateEvents(eventTypes)
|
||||
}
|
||||
}
|
||||
|
||||
fun liveReadMarker(): Observable<Optional<String>> {
|
||||
return room.getReadMarkerLive().asObservable()
|
||||
}
|
||||
|
@ -104,6 +112,10 @@ class RxRoom(private val room: Room) {
|
|||
room.invite(userId, reason, it)
|
||||
}
|
||||
|
||||
fun invite3pid(threePid: ThreePid): Completable = completableBuilder<Unit> {
|
||||
room.invite3pid(threePid, it)
|
||||
}
|
||||
|
||||
fun updateTopic(topic: String): Completable = completableBuilder<Unit> {
|
||||
room.updateTopic(topic, it)
|
||||
}
|
||||
|
|
|
@ -17,14 +17,20 @@
|
|||
package im.vector.matrix.rx
|
||||
|
||||
import androidx.paging.PagedList
|
||||
import im.vector.matrix.android.api.extensions.orFalse
|
||||
import im.vector.matrix.android.api.query.QueryStringValue
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
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.group.GroupSummaryQueryParams
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||
import im.vector.matrix.android.api.session.pushers.Pusher
|
||||
import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
|
||||
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.session.sync.SyncState
|
||||
|
@ -36,9 +42,11 @@ import im.vector.matrix.android.api.util.toOptional
|
|||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.functions.Function3
|
||||
|
||||
class RxSession(private val session: Session) {
|
||||
|
||||
|
@ -165,6 +173,42 @@ class RxSession(private val session: Session) {
|
|||
session.widgetService().getRoomWidgets(roomId, widgetId, widgetTypes, excludedTypes)
|
||||
}
|
||||
}
|
||||
|
||||
fun liveRoomChangeMembershipState(): Observable<Map<String, ChangeMembershipState>> {
|
||||
return session.getChangeMembershipsLive().asObservable()
|
||||
}
|
||||
|
||||
fun liveSecretSynchronisationInfo(): Observable<SecretsSynchronisationInfo> {
|
||||
return Observable.combineLatest<List<UserAccountData>, Optional<MXCrossSigningInfo>, Optional<PrivateKeysInfo>, SecretsSynchronisationInfo>(
|
||||
liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME)),
|
||||
liveCrossSigningInfo(session.myUserId),
|
||||
liveCrossSigningPrivateKeys(),
|
||||
Function3 { _, crossSigningInfo, pInfo ->
|
||||
// first check if 4S is already setup
|
||||
val is4SSetup = session.sharedSecretStorageService.isRecoverySetup()
|
||||
val isCrossSigningEnabled = crossSigningInfo.getOrNull() != null
|
||||
val isCrossSigningTrusted = crossSigningInfo.getOrNull()?.isTrusted() == true
|
||||
val allPrivateKeysKnown = pInfo.getOrNull()?.allKnown().orFalse()
|
||||
|
||||
val keysBackupService = session.cryptoService().keysBackupService()
|
||||
val currentBackupVersion = keysBackupService.currentBackupVersion
|
||||
val megolmBackupAvailable = currentBackupVersion != null
|
||||
val savedBackupKey = keysBackupService.getKeyBackupRecoveryKeyInfo()
|
||||
|
||||
val megolmKeyKnown = savedBackupKey?.version == currentBackupVersion
|
||||
SecretsSynchronisationInfo(
|
||||
isBackupSetup = is4SSetup,
|
||||
isCrossSigningEnabled = isCrossSigningEnabled,
|
||||
isCrossSigningTrusted = isCrossSigningTrusted,
|
||||
allPrivateKeysKnown = allPrivateKeysKnown,
|
||||
megolmBackupAvailable = megolmBackupAvailable,
|
||||
megolmSecretKnown = megolmKeyKnown,
|
||||
isMegolmKeyIn4S = session.sharedSecretStorageService.isMegolmKeyInBackup()
|
||||
)
|
||||
}
|
||||
)
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
}
|
||||
|
||||
fun Session.rx(): RxSession {
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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.matrix.rx
|
||||
|
||||
data class SecretsSynchronisationInfo(
|
||||
val isBackupSetup: Boolean,
|
||||
val isCrossSigningEnabled: Boolean,
|
||||
val isCrossSigningTrusted: Boolean,
|
||||
val allPrivateKeysKnown: Boolean,
|
||||
val megolmBackupAvailable: Boolean,
|
||||
val megolmSecretKnown: Boolean,
|
||||
val isMegolmKeyIn4S: Boolean
|
||||
)
|
|
@ -51,7 +51,6 @@ android {
|
|||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
debug {
|
||||
// Set to true to log privacy or sensible data, such as token
|
||||
buildConfigField "boolean", "LOG_PRIVATE_DATA", project.property("vector.debugPrivateData")
|
||||
|
@ -123,7 +122,7 @@ dependencies {
|
|||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
|
||||
implementation "androidx.appcompat:appcompat:1.1.0"
|
||||
implementation "androidx.core:core-ktx:1.1.0"
|
||||
implementation "androidx.core:core-ktx:1.3.0"
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
||||
|
@ -205,5 +204,4 @@ dependencies {
|
|||
androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
||||
|
||||
androidTestUtil 'androidx.test:orchestrator:1.2.0'
|
||||
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
|||
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
|
||||
|
||||
val roomId = mTestHelper.doSync<String> {
|
||||
aliceSession.createRoom(CreateRoomParams(name = "MyRoom"), it)
|
||||
aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" }, it)
|
||||
}
|
||||
|
||||
if (encryptedRoom) {
|
||||
|
@ -175,7 +175,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
|||
}
|
||||
|
||||
mTestHelper.doSync<Unit> {
|
||||
samSession.joinRoom(room.roomId, null, it)
|
||||
samSession.joinRoom(room.roomId, null, emptyList(), it)
|
||||
}
|
||||
|
||||
return samSession
|
||||
|
@ -286,9 +286,11 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
|||
fun createDM(alice: Session, bob: Session): String {
|
||||
val roomId = mTestHelper.doSync<String> {
|
||||
alice.createRoom(
|
||||
CreateRoomParams(invitedUserIds = listOf(bob.myUserId))
|
||||
.setDirectMessage()
|
||||
.enableEncryptionIfInvitedUsersSupportIt(),
|
||||
CreateRoomParams().apply {
|
||||
invitedUserIds.add(bob.myUserId)
|
||||
setDirectMessage()
|
||||
enableEncryptionIfInvitedUsersSupportIt = true
|
||||
},
|
||||
it
|
||||
)
|
||||
}
|
||||
|
|
|
@ -66,7 +66,10 @@ class KeyShareTests : InstrumentedTest {
|
|||
// Create an encrypted room and add a message
|
||||
val roomId = mTestHelper.doSync<String> {
|
||||
aliceSession.createRoom(
|
||||
CreateRoomParams(RoomDirectoryVisibility.PRIVATE).enableEncryptionWithAlgorithm(true),
|
||||
CreateRoomParams().apply {
|
||||
visibility = RoomDirectoryVisibility.PRIVATE
|
||||
enableEncryption()
|
||||
},
|
||||
it
|
||||
)
|
||||
}
|
||||
|
@ -285,7 +288,7 @@ class KeyShareTests : InstrumentedTest {
|
|||
mTestHelper.waitWithLatch(60_000) { latch ->
|
||||
val keysBackupService = aliceSession2.cryptoService().keysBackupService()
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
Log.d("#TEST", "Recovery :${ keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
|
||||
Log.d("#TEST", "Recovery :${keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
|
||||
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ data class MatrixConfiguration(
|
|||
),
|
||||
/**
|
||||
* Optional proxy to connect to the matrix servers
|
||||
* You can create one using for instance Proxy(proxyType, InetSocketAddress(hostname, port)
|
||||
* You can create one using for instance Proxy(proxyType, InetSocketAddress.createUnresolved(hostname, port)
|
||||
*/
|
||||
val proxy: Proxy? = null
|
||||
) {
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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.matrix.android.api.extensions
|
||||
|
||||
fun CharSequence.ensurePrefix(prefix: CharSequence): CharSequence {
|
||||
return when {
|
||||
startsWith(prefix) -> this
|
||||
else -> "$prefix$this"
|
||||
}
|
||||
}
|
|
@ -47,6 +47,7 @@ import im.vector.matrix.android.api.session.terms.TermsService
|
|||
import im.vector.matrix.android.api.session.typing.TypingUsersTracker
|
||||
import im.vector.matrix.android.api.session.user.UserService
|
||||
import im.vector.matrix.android.api.session.widgets.WidgetService
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
/**
|
||||
* This interface defines interactions with a session.
|
||||
|
@ -205,6 +206,13 @@ interface Session :
|
|||
*/
|
||||
fun removeListener(listener: Listener)
|
||||
|
||||
/**
|
||||
* Will return a OkHttpClient which will manage pinned certificates and Proxy if configured.
|
||||
* It will not add any access-token to the request.
|
||||
* So it is exposed to let the app be able to download image with Glide or any other libraries which accept an OkHttp client.
|
||||
*/
|
||||
fun getOkHttpClient(): OkHttpClient
|
||||
|
||||
/**
|
||||
* A global session listener to get notified for some events.
|
||||
*/
|
||||
|
|
|
@ -61,6 +61,8 @@ interface CrossSigningService {
|
|||
|
||||
fun canCrossSign(): Boolean
|
||||
|
||||
fun allPrivateKeysKnown(): Boolean
|
||||
|
||||
fun trustUser(otherUserId: String,
|
||||
callback: MatrixCallback<Unit>)
|
||||
|
||||
|
|
|
@ -39,5 +39,10 @@ data class UnsignedData(
|
|||
* Optional. The previous content for this event. If there is no previous content, this key will be missing.
|
||||
*/
|
||||
@Json(name = "prev_content") val prevContent: Map<String, Any>? = null,
|
||||
@Json(name = "m.relations") val relations: AggregatedRelations? = null
|
||||
@Json(name = "m.relations") val relations: AggregatedRelations? = null,
|
||||
/**
|
||||
* Optional. The eventId of the previous state event being replaced.
|
||||
*/
|
||||
@Json(name = "replaces_state") val replacesState: String? = null
|
||||
|
||||
)
|
||||
|
|
|
@ -16,9 +16,20 @@
|
|||
|
||||
package im.vector.matrix.android.api.session.group
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
/**
|
||||
* This interface defines methods to interact within a group.
|
||||
*/
|
||||
interface Group {
|
||||
val groupId: String
|
||||
|
||||
/**
|
||||
* This methods allows you to refresh data about this group. It will be reflected on the GroupSummary.
|
||||
* The SDK also takes care of refreshing group data every hour.
|
||||
* @param callback : the matrix callback to be notified of success or failure
|
||||
* @return a Cancelable to be able to cancel requests.
|
||||
*/
|
||||
fun fetchGroupData(callback: MatrixCallback<Unit>): Cancelable
|
||||
}
|
||||
|
|
|
@ -34,13 +34,6 @@ interface RoomDirectoryService {
|
|||
publicRoomsParams: PublicRoomsParams,
|
||||
callback: MatrixCallback<PublicRoomsResponse>): Cancelable
|
||||
|
||||
/**
|
||||
* Join a room by id, or room alias
|
||||
*/
|
||||
fun joinRoom(roomIdOrAlias: String,
|
||||
reason: String? = null,
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Fetches the overall metadata about protocols supported by the homeserver.
|
||||
* Includes both the available protocols and all fields required for queries against each protocol.
|
||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room
|
|||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
@ -104,5 +105,13 @@ interface RoomService {
|
|||
searchOnServer: Boolean,
|
||||
callback: MatrixCallback<Optional<String>>): Cancelable
|
||||
|
||||
fun getExistingDirectRoomWithUser(otherUserId: String) : Room?
|
||||
/**
|
||||
* Return a live data of all local changes membership that happened since the session has been opened.
|
||||
* It allows you to track this in your client to known what is currently being processed by the SDK.
|
||||
* It won't know anything about change being done in other client.
|
||||
* Keys are roomId or roomAlias, depending of what you used as parameter for the join/leave action
|
||||
*/
|
||||
fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>>
|
||||
|
||||
fun getExistingDirectRoomWithUser(otherUserId: String): Room?
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {
|
|||
* [im.vector.matrix.android.api.session.room.Room] and [im.vector.matrix.android.api.session.room.RoomService]
|
||||
*/
|
||||
data class RoomSummaryQueryParams(
|
||||
val roomId: QueryStringValue,
|
||||
val displayName: QueryStringValue,
|
||||
val canonicalAlias: QueryStringValue,
|
||||
val memberships: List<Membership>
|
||||
|
@ -35,11 +36,13 @@ data class RoomSummaryQueryParams(
|
|||
|
||||
class Builder {
|
||||
|
||||
var roomId: QueryStringValue = QueryStringValue.IsNotEmpty
|
||||
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
|
||||
var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition
|
||||
var memberships: List<Membership> = Membership.all()
|
||||
|
||||
fun build() = RoomSummaryQueryParams(
|
||||
roomId = roomId,
|
||||
displayName = displayName,
|
||||
canonicalAlias = canonicalAlias,
|
||||
memberships = memberships
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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.matrix.android.api.session.room.members
|
||||
|
||||
sealed class ChangeMembershipState() {
|
||||
object Unknown : ChangeMembershipState()
|
||||
object Joining : ChangeMembershipState()
|
||||
data class FailedJoining(val throwable: Throwable) : ChangeMembershipState()
|
||||
object Joined : ChangeMembershipState()
|
||||
object Leaving : ChangeMembershipState()
|
||||
data class FailedLeaving(val throwable: Throwable) : ChangeMembershipState()
|
||||
object Left : ChangeMembershipState()
|
||||
|
||||
fun isInProgress() = this is Joining || this is Leaving
|
||||
|
||||
fun isSuccessful() = this is Joined || this is Left
|
||||
|
||||
fun isFailed() = this is FailedJoining || this is FailedLeaving
|
||||
}
|
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room.members
|
|||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
|
@ -63,6 +64,12 @@ interface MembershipService {
|
|||
reason: String? = null,
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Invite a user with email or phone number in the room
|
||||
*/
|
||||
fun invite3pid(threePid: ThreePid,
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Ban a user from the room
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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.matrix.android.api.session.room.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Class representing the EventType.STATE_ROOM_THIRD_PARTY_INVITE state event content
|
||||
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#m-room-third-party-invite
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RoomThirdPartyInviteContent(
|
||||
/**
|
||||
* Required. A user-readable string which represents the user who has been invited.
|
||||
* This should not contain the user's third party ID, as otherwise when the invite
|
||||
* is accepted it would leak the association between the matrix ID and the third party ID.
|
||||
*/
|
||||
@Json(name = "display_name") val displayName: String,
|
||||
|
||||
/**
|
||||
* Required. A URL which can be fetched, with querystring public_key=public_key, to validate
|
||||
* whether the key has been revoked. The URL must return a JSON object containing a boolean property named 'valid'.
|
||||
*/
|
||||
@Json(name = "key_validity_url") val keyValidityUrl: String,
|
||||
|
||||
/**
|
||||
* Required. A base64-encoded ed25519 key with which token must be signed (though a signature from any entry in
|
||||
* public_keys is also sufficient). This exists for backwards compatibility.
|
||||
*/
|
||||
@Json(name = "public_key") val publicKey: String,
|
||||
|
||||
/**
|
||||
* Keys with which the token may be signed.
|
||||
*/
|
||||
@Json(name = "public_keys") val publicKeys: List<PublicKeys> = emptyList()
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PublicKeys(
|
||||
/**
|
||||
* An optional URL which can be fetched, with querystring public_key=public_key, to validate whether the key
|
||||
* has been revoked. The URL must return a JSON object containing a boolean property named 'valid'. If this URL
|
||||
* is absent, the key must be considered valid indefinitely.
|
||||
*/
|
||||
@Json(name = "key_validity_url") val keyValidityUrl: String? = null,
|
||||
|
||||
/**
|
||||
* Required. A base-64 encoded ed25519 key with which token may be signed.
|
||||
*/
|
||||
@Json(name = "public_key") val publicKey: String
|
||||
)
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
* 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.
|
||||
|
@ -16,253 +16,102 @@
|
|||
|
||||
package im.vector.matrix.android.api.session.room.model.create
|
||||
|
||||
import android.util.Patterns
|
||||
import androidx.annotation.CheckResult
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.MatrixPatterns.isUserId
|
||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
|
||||
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
|
||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
|
||||
import im.vector.matrix.android.internal.auth.data.ThreePidMedium
|
||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Parameter to create a room, with facilities functions to configure it
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CreateRoomParams(
|
||||
/**
|
||||
* A public visibility indicates that the room will be shown in the published room list.
|
||||
* A private visibility will hide the room from the published room list.
|
||||
* Rooms default to private visibility if this key is not included.
|
||||
* NB: This should not be confused with join_rules which also uses the word public. One of: ["public", "private"]
|
||||
*/
|
||||
@Json(name = "visibility")
|
||||
val visibility: RoomDirectoryVisibility? = null,
|
||||
|
||||
/**
|
||||
* The desired room alias local part. If this is included, a room alias will be created and mapped to the newly created room.
|
||||
* The alias will belong on the same homeserver which created the room.
|
||||
* For example, if this was set to "foo" and sent to the homeserver "example.com" the complete room alias would be #foo:example.com.
|
||||
*/
|
||||
@Json(name = "room_alias_name")
|
||||
val roomAliasName: String? = null,
|
||||
|
||||
/**
|
||||
* If this is included, an m.room.name event will be sent into the room to indicate the name of the room.
|
||||
* See Room Events for more information on m.room.name.
|
||||
*/
|
||||
@Json(name = "name")
|
||||
val name: String? = null,
|
||||
|
||||
/**
|
||||
* If this is included, an m.room.topic event will be sent into the room to indicate the topic for the room.
|
||||
* See Room Events for more information on m.room.topic.
|
||||
*/
|
||||
@Json(name = "topic")
|
||||
val topic: String? = null,
|
||||
|
||||
/**
|
||||
* A list of user IDs to invite to the room.
|
||||
* This will tell the server to invite everyone in the list to the newly created room.
|
||||
*/
|
||||
@Json(name = "invite")
|
||||
val invitedUserIds: List<String>? = null,
|
||||
|
||||
/**
|
||||
* A list of objects representing third party IDs to invite into the room.
|
||||
*/
|
||||
@Json(name = "invite_3pid")
|
||||
val invite3pids: List<Invite3Pid>? = null,
|
||||
|
||||
/**
|
||||
* Extra keys to be added to the content of the m.room.create.
|
||||
* The server will clobber the following keys: creator.
|
||||
* Future versions of the specification may allow the server to clobber other keys.
|
||||
*/
|
||||
@Json(name = "creation_content")
|
||||
val creationContent: Any? = null,
|
||||
|
||||
/**
|
||||
* A list of state events to set in the new room.
|
||||
* This allows the user to override the default state events set in the new room.
|
||||
* The expected format of the state events are an object with type, state_key and content keys set.
|
||||
* Takes precedence over events set by presets, but gets overridden by name and topic keys.
|
||||
*/
|
||||
@Json(name = "initial_state")
|
||||
val initialStates: List<Event>? = null,
|
||||
|
||||
/**
|
||||
* Convenience parameter for setting various default state events based on a preset. Must be either:
|
||||
* private_chat => join_rules is set to invite. history_visibility is set to shared.
|
||||
* trusted_private_chat => join_rules is set to invite. history_visibility is set to shared. All invitees are given the same power level as the
|
||||
* room creator.
|
||||
* public_chat: => join_rules is set to public. history_visibility is set to shared.
|
||||
*/
|
||||
@Json(name = "preset")
|
||||
val preset: CreateRoomPreset? = null,
|
||||
|
||||
/**
|
||||
* This flag makes the server set the is_direct flag on the m.room.member events sent to the users in invite and invite_3pid.
|
||||
* See Direct Messaging for more information.
|
||||
*/
|
||||
@Json(name = "is_direct")
|
||||
val isDirect: Boolean? = null,
|
||||
|
||||
/**
|
||||
* The power level content to override in the default power level event
|
||||
*/
|
||||
@Json(name = "power_level_content_override")
|
||||
val powerLevelContentOverride: PowerLevelsContent? = null
|
||||
) {
|
||||
@Transient
|
||||
internal var enableEncryptionIfInvitedUsersSupportIt: Boolean = false
|
||||
private set
|
||||
// TODO Give a way to include other initial states
|
||||
class CreateRoomParams {
|
||||
/**
|
||||
* A public visibility indicates that the room will be shown in the published room list.
|
||||
* A private visibility will hide the room from the published room list.
|
||||
* Rooms default to private visibility if this key is not included.
|
||||
* NB: This should not be confused with join_rules which also uses the word public. One of: ["public", "private"]
|
||||
*/
|
||||
var visibility: RoomDirectoryVisibility? = null
|
||||
|
||||
/**
|
||||
* After calling this method, when the room will be created, if cross-signing is enabled and we can get keys for every invited users,
|
||||
* The desired room alias local part. If this is included, a room alias will be created and mapped to the newly created room.
|
||||
* The alias will belong on the same homeserver which created the room.
|
||||
* For example, if this was set to "foo" and sent to the homeserver "example.com" the complete room alias would be #foo:example.com.
|
||||
*/
|
||||
var roomAliasName: String? = null
|
||||
|
||||
/**
|
||||
* If this is not null, an m.room.name event will be sent into the room to indicate the name of the room.
|
||||
* See Room Events for more information on m.room.name.
|
||||
*/
|
||||
var name: String? = null
|
||||
|
||||
/**
|
||||
* If this is not null, an m.room.topic event will be sent into the room to indicate the topic for the room.
|
||||
* See Room Events for more information on m.room.topic.
|
||||
*/
|
||||
var topic: String? = null
|
||||
|
||||
/**
|
||||
* A list of user IDs to invite to the room.
|
||||
* This will tell the server to invite everyone in the list to the newly created room.
|
||||
*/
|
||||
val invitedUserIds = mutableListOf<String>()
|
||||
|
||||
/**
|
||||
* A list of objects representing third party IDs to invite into the room.
|
||||
*/
|
||||
val invite3pids = mutableListOf<ThreePid>()
|
||||
|
||||
/**
|
||||
* If set to true, when the room will be created, if cross-signing is enabled and we can get keys for every invited users,
|
||||
* the encryption will be enabled on the created room
|
||||
* @param value true to activate this behavior.
|
||||
* @return this, to allow chaining methods
|
||||
*/
|
||||
fun enableEncryptionIfInvitedUsersSupportIt(value: Boolean = true): CreateRoomParams {
|
||||
enableEncryptionIfInvitedUsersSupportIt = value
|
||||
return this
|
||||
}
|
||||
var enableEncryptionIfInvitedUsersSupportIt: Boolean = false
|
||||
|
||||
/**
|
||||
* Add the crypto algorithm to the room creation parameters.
|
||||
*
|
||||
* @param enable true to enable encryption.
|
||||
* @param algorithm the algorithm, default to [MXCRYPTO_ALGORITHM_MEGOLM], which is actually the only supported algorithm for the moment
|
||||
* @return a modified copy of the CreateRoomParams object, or this if there is no modification
|
||||
* Convenience parameter for setting various default state events based on a preset. Must be either:
|
||||
* private_chat => join_rules is set to invite. history_visibility is set to shared.
|
||||
* trusted_private_chat => join_rules is set to invite. history_visibility is set to shared. All invitees are given the same power level as the
|
||||
* room creator.
|
||||
* public_chat: => join_rules is set to public. history_visibility is set to shared.
|
||||
*/
|
||||
@CheckResult
|
||||
fun enableEncryptionWithAlgorithm(enable: Boolean = true,
|
||||
algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM): CreateRoomParams {
|
||||
// Remove the existing value if any.
|
||||
val newInitialStates = initialStates
|
||||
?.filter { it.type != EventType.STATE_ROOM_ENCRYPTION }
|
||||
|
||||
return if (algorithm == MXCRYPTO_ALGORITHM_MEGOLM) {
|
||||
if (enable) {
|
||||
val contentMap = mapOf("algorithm" to algorithm)
|
||||
|
||||
val algoEvent = Event(
|
||||
type = EventType.STATE_ROOM_ENCRYPTION,
|
||||
stateKey = "",
|
||||
content = contentMap.toContent()
|
||||
)
|
||||
|
||||
copy(
|
||||
initialStates = newInitialStates.orEmpty() + algoEvent
|
||||
)
|
||||
} else {
|
||||
return copy(
|
||||
initialStates = newInitialStates
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Timber.e("Unsupported algorithm: $algorithm")
|
||||
this
|
||||
}
|
||||
}
|
||||
var preset: CreateRoomPreset? = null
|
||||
|
||||
/**
|
||||
* Force the history visibility in the room creation parameters.
|
||||
*
|
||||
* @param historyVisibility the expected history visibility, set null to remove any existing value.
|
||||
* @return a modified copy of the CreateRoomParams object
|
||||
* This flag makes the server set the is_direct flag on the m.room.member events sent to the users in invite and invite_3pid.
|
||||
* See Direct Messaging for more information.
|
||||
*/
|
||||
@CheckResult
|
||||
fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?): CreateRoomParams {
|
||||
// Remove the existing value if any.
|
||||
val newInitialStates = initialStates
|
||||
?.filter { it.type != EventType.STATE_ROOM_HISTORY_VISIBILITY }
|
||||
var isDirect: Boolean? = null
|
||||
|
||||
if (historyVisibility != null) {
|
||||
val contentMap = mapOf("history_visibility" to historyVisibility)
|
||||
/**
|
||||
* Extra keys to be added to the content of the m.room.create.
|
||||
* The server will clobber the following keys: creator.
|
||||
* Future versions of the specification may allow the server to clobber other keys.
|
||||
*/
|
||||
var creationContent: Any? = null
|
||||
|
||||
val historyVisibilityEvent = Event(
|
||||
type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||
stateKey = "",
|
||||
content = contentMap.toContent())
|
||||
|
||||
return copy(
|
||||
initialStates = newInitialStates.orEmpty() + historyVisibilityEvent
|
||||
)
|
||||
} else {
|
||||
return copy(
|
||||
initialStates = newInitialStates
|
||||
)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* The power level content to override in the default power level event
|
||||
*/
|
||||
var powerLevelContentOverride: PowerLevelsContent? = null
|
||||
|
||||
/**
|
||||
* Mark as a direct message room.
|
||||
* @return a modified copy of the CreateRoomParams object
|
||||
*/
|
||||
@CheckResult
|
||||
fun setDirectMessage(): CreateRoomParams {
|
||||
return copy(
|
||||
preset = CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT,
|
||||
isDirect = true
|
||||
)
|
||||
fun setDirectMessage() {
|
||||
preset = CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
|
||||
isDirect = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the created room can be a direct chat one.
|
||||
*
|
||||
* @return true if it is a direct chat
|
||||
* Supported value: MXCRYPTO_ALGORITHM_MEGOLM
|
||||
*/
|
||||
fun isDirect(): Boolean {
|
||||
return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
|
||||
&& isDirect == true
|
||||
}
|
||||
var algorithm: String? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* @return the first invited user id
|
||||
*/
|
||||
fun getFirstInvitedUserId(): String? {
|
||||
return invitedUserIds?.firstOrNull() ?: invite3pids?.firstOrNull()?.address
|
||||
}
|
||||
var historyVisibility: RoomHistoryVisibility? = null
|
||||
|
||||
/**
|
||||
* Add some ids to the room creation
|
||||
* ids might be a matrix id or an email address.
|
||||
*
|
||||
* @param ids the participant ids to add.
|
||||
* @return a modified copy of the CreateRoomParams object
|
||||
*/
|
||||
@CheckResult
|
||||
fun addParticipantIds(hsConfig: HomeServerConnectionConfig,
|
||||
userId: String,
|
||||
ids: List<String>): CreateRoomParams {
|
||||
return copy(
|
||||
invite3pids = (invite3pids.orEmpty() + ids
|
||||
.takeIf { hsConfig.identityServerUri != null }
|
||||
?.filter { id -> Patterns.EMAIL_ADDRESS.matcher(id).matches() }
|
||||
?.map { id ->
|
||||
Invite3Pid(
|
||||
idServer = hsConfig.identityServerUri!!.host!!,
|
||||
medium = ThreePidMedium.EMAIL,
|
||||
address = id
|
||||
)
|
||||
}
|
||||
.orEmpty())
|
||||
.distinct(),
|
||||
invitedUserIds = (invitedUserIds.orEmpty() + ids
|
||||
.filter { id -> isUserId(id) }
|
||||
// do not invite oneself
|
||||
.filter { id -> id != userId })
|
||||
.distinct()
|
||||
)
|
||||
// TODO add phonenumbers when it will be available
|
||||
fun enableEncryption() {
|
||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,42 +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.matrix.android.api.session.room.model.create
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Invite3Pid(
|
||||
/**
|
||||
* Required.
|
||||
* The hostname+port of the identity server which should be used for third party identifier lookups.
|
||||
*/
|
||||
@Json(name = "id_server")
|
||||
val idServer: String,
|
||||
|
||||
/**
|
||||
* Required.
|
||||
* The kind of address being passed in the address field, for example email.
|
||||
*/
|
||||
val medium: String,
|
||||
|
||||
/**
|
||||
* Required.
|
||||
* The invitee's third party identifier.
|
||||
*/
|
||||
val address: String
|
||||
)
|
|
@ -17,7 +17,6 @@
|
|||
|
||||
package im.vector.matrix.android.api.session.room.powerlevels
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
|
||||
|
||||
/**
|
||||
|
@ -124,59 +123,4 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
|
|||
else -> Role.Moderator.value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user have the necessary power level to change room name
|
||||
* @param userId the id of the user to check for.
|
||||
* @return true if able to change room name
|
||||
*/
|
||||
fun isUserAbleToChangeRoomName(userId: String): Boolean {
|
||||
val powerLevel = getUserPowerLevelValue(userId)
|
||||
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_NAME] ?: powerLevelsContent.stateDefault
|
||||
return powerLevel >= minPowerLevel
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user have the necessary power level to change room topic
|
||||
* @param userId the id of the user to check for.
|
||||
* @return true if able to change room topic
|
||||
*/
|
||||
fun isUserAbleToChangeRoomTopic(userId: String): Boolean {
|
||||
val powerLevel = getUserPowerLevelValue(userId)
|
||||
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_TOPIC] ?: powerLevelsContent.stateDefault
|
||||
return powerLevel >= minPowerLevel
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user have the necessary power level to change room canonical alias
|
||||
* @param userId the id of the user to check for.
|
||||
* @return true if able to change room canonical alias
|
||||
*/
|
||||
fun isUserAbleToChangeRoomCanonicalAlias(userId: String): Boolean {
|
||||
val powerLevel = getUserPowerLevelValue(userId)
|
||||
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_CANONICAL_ALIAS] ?: powerLevelsContent.stateDefault
|
||||
return powerLevel >= minPowerLevel
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user have the necessary power level to change room history readability
|
||||
* @param userId the id of the user to check for.
|
||||
* @return true if able to change room history readability
|
||||
*/
|
||||
fun isUserAbleToChangeRoomHistoryReadability(userId: String): Boolean {
|
||||
val powerLevel = getUserPowerLevelValue(userId)
|
||||
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_HISTORY_VISIBILITY] ?: powerLevelsContent.stateDefault
|
||||
return powerLevel >= minPowerLevel
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user have the necessary power level to change room avatar
|
||||
* @param userId the id of the user to check for.
|
||||
* @return true if able to change room avatar
|
||||
*/
|
||||
fun isUserAbleToChangeRoomAvatar(userId: String): Boolean {
|
||||
val powerLevel = getUserPowerLevelValue(userId)
|
||||
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_AVATAR] ?: powerLevelsContent.stateDefault
|
||||
return powerLevel >= minPowerLevel
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,4 +39,6 @@ interface TimelineService {
|
|||
fun getTimeLineEvent(eventId: String): TimelineEvent?
|
||||
|
||||
fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>>
|
||||
|
||||
fun getAttachmentMessages() : List<TimelineEvent>
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.securestorage
|
|||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||
|
@ -124,6 +125,13 @@ interface SharedSecretStorageService {
|
|||
) is IntegrityResult.Success
|
||||
}
|
||||
|
||||
fun isMegolmKeyInBackup(): Boolean {
|
||||
return checkShouldBeAbleToAccessSecrets(
|
||||
secretNames = listOf(KEYBACKUP_SECRET_SSSS_NAME),
|
||||
keyId = null
|
||||
) is IntegrityResult.Success
|
||||
}
|
||||
|
||||
fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?): IntegrityResult
|
||||
|
||||
fun requestSecret(name: String, myOtherDeviceId: String)
|
||||
|
|
|
@ -71,8 +71,8 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
|
|||
delay(1500)
|
||||
cryptoStore.getOrAddOutgoingSecretShareRequest(secretName, recipients)?.let {
|
||||
// TODO check if there is already one that is being sent?
|
||||
if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
|
||||
Timber.v("## CRYPTO - GOSSIP sendSecretShareRequest() : we already request for that session: $it")
|
||||
if (it.state == OutgoingGossipingRequestState.SENDING /**|| it.state == OutgoingGossipingRequestState.SENT*/) {
|
||||
Timber.v("## CRYPTO - GOSSIP sendSecretShareRequest() : we are already sending for that session: $it")
|
||||
return@launch
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.crosssigning
|
|||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.extensions.orFalse
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
|
@ -507,6 +508,11 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||
&& cryptoStore.getCrossSigningPrivateKeys()?.user != null
|
||||
}
|
||||
|
||||
override fun allPrivateKeysKnown(): Boolean {
|
||||
return checkSelfTrust().isVerified()
|
||||
&& cryptoStore.getCrossSigningPrivateKeys()?.allKnown().orFalse()
|
||||
}
|
||||
|
||||
override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
Timber.d("## CrossSigning - Mark user $userId as trusted ")
|
||||
|
|
|
@ -20,4 +20,6 @@ data class PrivateKeysInfo(
|
|||
val master: String? = null,
|
||||
val selfSigned: String? = null,
|
||||
val user: String? = null
|
||||
)
|
||||
) {
|
||||
fun allKnown() = master != null && selfSigned != null && user != null
|
||||
}
|
||||
|
|
|
@ -1,161 +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.matrix.android.internal.crypto.tasks
|
||||
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationReadyContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService
|
||||
import im.vector.matrix.android.internal.di.DeviceId
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import timber.log.Timber
|
||||
import java.util.ArrayList
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface RoomVerificationUpdateTask : Task<RoomVerificationUpdateTask.Params, Unit> {
|
||||
data class Params(
|
||||
val events: List<Event>,
|
||||
val verificationService: DefaultVerificationService,
|
||||
val cryptoService: CryptoService
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultRoomVerificationUpdateTask @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
@DeviceId private val deviceId: String?,
|
||||
private val cryptoService: CryptoService) : RoomVerificationUpdateTask {
|
||||
|
||||
companion object {
|
||||
// XXX what about multi-account?
|
||||
private val transactionsHandledByOtherDevice = ArrayList<String>()
|
||||
}
|
||||
|
||||
override suspend fun execute(params: RoomVerificationUpdateTask.Params) {
|
||||
// TODO ignore initial sync or back pagination?
|
||||
|
||||
params.events.forEach { event ->
|
||||
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
|
||||
|
||||
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
|
||||
// the message should be ignored by the receiver.
|
||||
|
||||
if (!VerificationService.isValidRequest(event.ageLocalTs
|
||||
?: event.originServerTs)) return@forEach Unit.also {
|
||||
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated")
|
||||
}
|
||||
|
||||
// decrypt if needed?
|
||||
if (event.isEncrypted() && event.mxDecryptionResult == null) {
|
||||
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
|
||||
// for now decrypt sync
|
||||
try {
|
||||
val result = cryptoService.decryptEvent(event, "")
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
)
|
||||
} catch (e: MXCryptoError) {
|
||||
Timber.e("## SAS Failed to decrypt event: ${event.eventId}")
|
||||
params.verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event)
|
||||
}
|
||||
}
|
||||
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
|
||||
|
||||
// Relates to is not encrypted
|
||||
val relatesToEventId = event.content.toModel<MessageRelationContent>()?.relatesTo?.eventId
|
||||
|
||||
if (event.senderId == userId) {
|
||||
// If it's send from me, we need to keep track of Requests or Start
|
||||
// done from another device of mine
|
||||
|
||||
if (EventType.MESSAGE == event.getClearType()) {
|
||||
val msgType = event.getClearContent().toModel<MessageContent>()?.msgType
|
||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
|
||||
event.getClearContent().toModel<MessageVerificationRequestContent>()?.let {
|
||||
if (it.fromDevice != deviceId) {
|
||||
// The verification is requested from another device
|
||||
Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ")
|
||||
event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (EventType.KEY_VERIFICATION_START == event.getClearType()) {
|
||||
event.getClearContent().toModel<MessageVerificationStartContent>()?.let {
|
||||
if (it.fromDevice != deviceId) {
|
||||
// The verification is started from another device
|
||||
Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ")
|
||||
relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
||||
params.verificationService.onRoomRequestHandledByOtherDevice(event)
|
||||
}
|
||||
}
|
||||
} else if (EventType.KEY_VERIFICATION_READY == event.getClearType()) {
|
||||
event.getClearContent().toModel<MessageVerificationReadyContent>()?.let {
|
||||
if (it.fromDevice != deviceId) {
|
||||
// The verification is started from another device
|
||||
Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ")
|
||||
relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
||||
params.verificationService.onRoomRequestHandledByOtherDevice(event)
|
||||
}
|
||||
}
|
||||
} else if (EventType.KEY_VERIFICATION_CANCEL == event.getClearType() || EventType.KEY_VERIFICATION_DONE == event.getClearType()) {
|
||||
relatesToEventId?.let {
|
||||
transactionsHandledByOtherDevice.remove(it)
|
||||
params.verificationService.onRoomRequestHandledByOtherDevice(event)
|
||||
}
|
||||
}
|
||||
|
||||
Timber.v("## SAS Verification ignoring message sent by me: ${event.eventId} type: ${event.getClearType()}")
|
||||
return@forEach
|
||||
}
|
||||
|
||||
if (relatesToEventId != null && transactionsHandledByOtherDevice.contains(relatesToEventId)) {
|
||||
// Ignore this event, it is directed to another of my devices
|
||||
Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesToEventId ")
|
||||
return@forEach
|
||||
}
|
||||
when (event.getClearType()) {
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_DONE -> {
|
||||
params.verificationService.onRoomEvent(event)
|
||||
}
|
||||
EventType.MESSAGE -> {
|
||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.msgType) {
|
||||
params.verificationService.onRoomRequestReceived(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1234,7 +1234,7 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
)
|
||||
|
||||
// We can SCAN or SHOW QR codes only if cross-signing is enabled
|
||||
val methodValues = if (crossSigningService.isCrossSigningVerified()) {
|
||||
val methodValues = if (crossSigningService.isCrossSigningInitialized()) {
|
||||
// Add reciprocate method if application declares it can scan or show QR codes
|
||||
// Not sure if it ok to do that (?)
|
||||
val reciprocateMethod = methods
|
||||
|
|
|
@ -1,73 +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.matrix.android.internal.crypto.verification
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultRoomVerificationUpdateTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.RoomVerificationUpdateTask
|
||||
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.query.whereTypes
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import io.realm.OrderedCollectionChangeSet
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmResults
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class VerificationMessageLiveObserver @Inject constructor(
|
||||
@SessionDatabase realmConfiguration: RealmConfiguration,
|
||||
private val roomVerificationUpdateTask: DefaultRoomVerificationUpdateTask,
|
||||
private val cryptoService: CryptoService,
|
||||
private val verificationService: DefaultVerificationService,
|
||||
private val taskExecutor: TaskExecutor
|
||||
) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
||||
|
||||
override val query = Monarchy.Query {
|
||||
EventEntity.whereTypes(it, listOf(
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.MESSAGE,
|
||||
EventType.ENCRYPTED)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
|
||||
// Should we ignore when it's an initial sync?
|
||||
val events = changeSet.insertions
|
||||
.asSequence()
|
||||
.mapNotNull { results[it]?.asDomain() }
|
||||
.filterNot {
|
||||
// ignore local echos
|
||||
LocalEcho.isLocalEchoId(it.eventId ?: "")
|
||||
}
|
||||
.toList()
|
||||
|
||||
roomVerificationUpdateTask.configureWith(
|
||||
RoomVerificationUpdateTask.Params(events, verificationService, cryptoService)
|
||||
).executeBy(taskExecutor)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* 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.matrix.android.internal.crypto.verification
|
||||
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationReadyContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.database.model.EventInsertType
|
||||
import im.vector.matrix.android.internal.di.DeviceId
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.session.EventInsertLiveProcessor
|
||||
import io.realm.Realm
|
||||
import timber.log.Timber
|
||||
import java.util.ArrayList
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class VerificationMessageProcessor @Inject constructor(
|
||||
private val cryptoService: CryptoService,
|
||||
private val verificationService: DefaultVerificationService,
|
||||
@UserId private val userId: String,
|
||||
@DeviceId private val deviceId: String?
|
||||
) : EventInsertLiveProcessor {
|
||||
|
||||
private val transactionsHandledByOtherDevice = ArrayList<String>()
|
||||
|
||||
private val allowedTypes = listOf(
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.MESSAGE,
|
||||
EventType.ENCRYPTED
|
||||
)
|
||||
|
||||
override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
|
||||
if (insertType != EventInsertType.INCREMENTAL_SYNC) {
|
||||
return false
|
||||
}
|
||||
return allowedTypes.contains(eventType) && !LocalEcho.isLocalEchoId(eventId)
|
||||
}
|
||||
|
||||
override suspend fun process(realm: Realm, event: Event) {
|
||||
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
|
||||
|
||||
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
|
||||
// the message should be ignored by the receiver.
|
||||
|
||||
if (!VerificationService.isValidRequest(event.ageLocalTs
|
||||
?: event.originServerTs)) return Unit.also {
|
||||
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated")
|
||||
}
|
||||
|
||||
// decrypt if needed?
|
||||
if (event.isEncrypted() && event.mxDecryptionResult == null) {
|
||||
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
|
||||
// for now decrypt sync
|
||||
try {
|
||||
val result = cryptoService.decryptEvent(event, "")
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
)
|
||||
} catch (e: MXCryptoError) {
|
||||
Timber.e("## SAS Failed to decrypt event: ${event.eventId}")
|
||||
verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event)
|
||||
}
|
||||
}
|
||||
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
|
||||
|
||||
// Relates to is not encrypted
|
||||
val relatesToEventId = event.content.toModel<MessageRelationContent>()?.relatesTo?.eventId
|
||||
|
||||
if (event.senderId == userId) {
|
||||
// If it's send from me, we need to keep track of Requests or Start
|
||||
// done from another device of mine
|
||||
|
||||
if (EventType.MESSAGE == event.getClearType()) {
|
||||
val msgType = event.getClearContent().toModel<MessageContent>()?.msgType
|
||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
|
||||
event.getClearContent().toModel<MessageVerificationRequestContent>()?.let {
|
||||
if (it.fromDevice != deviceId) {
|
||||
// The verification is requested from another device
|
||||
Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ")
|
||||
event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (EventType.KEY_VERIFICATION_START == event.getClearType()) {
|
||||
event.getClearContent().toModel<MessageVerificationStartContent>()?.let {
|
||||
if (it.fromDevice != deviceId) {
|
||||
// The verification is started from another device
|
||||
Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ")
|
||||
relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
||||
verificationService.onRoomRequestHandledByOtherDevice(event)
|
||||
}
|
||||
}
|
||||
} else if (EventType.KEY_VERIFICATION_READY == event.getClearType()) {
|
||||
event.getClearContent().toModel<MessageVerificationReadyContent>()?.let {
|
||||
if (it.fromDevice != deviceId) {
|
||||
// The verification is started from another device
|
||||
Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ")
|
||||
relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
||||
verificationService.onRoomRequestHandledByOtherDevice(event)
|
||||
}
|
||||
}
|
||||
} else if (EventType.KEY_VERIFICATION_CANCEL == event.getClearType() || EventType.KEY_VERIFICATION_DONE == event.getClearType()) {
|
||||
relatesToEventId?.let {
|
||||
transactionsHandledByOtherDevice.remove(it)
|
||||
verificationService.onRoomRequestHandledByOtherDevice(event)
|
||||
}
|
||||
}
|
||||
|
||||
Timber.v("## SAS Verification ignoring message sent by me: ${event.eventId} type: ${event.getClearType()}")
|
||||
return
|
||||
}
|
||||
|
||||
if (relatesToEventId != null && transactionsHandledByOtherDevice.contains(relatesToEventId)) {
|
||||
// Ignore this event, it is directed to another of my devices
|
||||
Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesToEventId ")
|
||||
return
|
||||
}
|
||||
when (event.getClearType()) {
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_DONE -> {
|
||||
verificationService.onRoomEvent(event)
|
||||
}
|
||||
EventType.MESSAGE -> {
|
||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.msgType) {
|
||||
verificationService.onRoomRequestReceived(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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.matrix.android.internal.database
|
||||
|
||||
import im.vector.matrix.android.internal.database.helper.nextDisplayIndex
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.session.SessionLifecycleObserver
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val MAX_NUMBER_OF_EVENTS_IN_DB = 35_000L
|
||||
private const val MIN_NUMBER_OF_EVENTS_BY_CHUNK = 300
|
||||
|
||||
/**
|
||||
* This class makes sure to stay under a maximum number of events as it makes Realm to be unusable when listening to events
|
||||
* when the database is getting too big. This will try incrementally to remove the biggest chunks until we get below the threshold.
|
||||
* We make sure to still have a minimum number of events so it's not becoming unusable.
|
||||
* So this won't work for users with a big number of very active rooms.
|
||||
*/
|
||||
internal class DatabaseCleaner @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration,
|
||||
private val taskExecutor: TaskExecutor) : SessionLifecycleObserver {
|
||||
|
||||
override fun onStart() {
|
||||
taskExecutor.executorScope.launch(Dispatchers.Default) {
|
||||
awaitTransaction(realmConfiguration) { realm ->
|
||||
val allRooms = realm.where(RoomEntity::class.java).findAll()
|
||||
Timber.v("There are ${allRooms.size} rooms in this session")
|
||||
cleanUp(realm, MAX_NUMBER_OF_EVENTS_IN_DB / 2L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun cleanUp(realm: Realm, threshold: Long) {
|
||||
val numberOfEvents = realm.where(EventEntity::class.java).findAll().size
|
||||
val numberOfTimelineEvents = realm.where(TimelineEventEntity::class.java).findAll().size
|
||||
Timber.v("Number of events in db: $numberOfEvents | Number of timeline events in db: $numberOfTimelineEvents")
|
||||
if (threshold <= MIN_NUMBER_OF_EVENTS_BY_CHUNK || numberOfTimelineEvents < MAX_NUMBER_OF_EVENTS_IN_DB) {
|
||||
Timber.v("Db is low enough")
|
||||
} else {
|
||||
val thresholdChunks = realm.where(ChunkEntity::class.java)
|
||||
.greaterThan(ChunkEntityFields.NUMBER_OF_TIMELINE_EVENTS, threshold)
|
||||
.findAll()
|
||||
|
||||
Timber.v("There are ${thresholdChunks.size} chunks to clean with more than $threshold events")
|
||||
for (chunk in thresholdChunks) {
|
||||
val maxDisplayIndex = chunk.nextDisplayIndex(PaginationDirection.FORWARDS)
|
||||
val thresholdDisplayIndex = maxDisplayIndex - threshold
|
||||
val eventsToRemove = chunk.timelineEvents.where().lessThan(TimelineEventEntityFields.DISPLAY_INDEX, thresholdDisplayIndex).findAll()
|
||||
Timber.v("There are ${eventsToRemove.size} events to clean in chunk: ${chunk.identifier()} from room ${chunk.room?.first()?.roomId}")
|
||||
chunk.numberOfTimelineEvents = chunk.numberOfTimelineEvents - eventsToRemove.size
|
||||
eventsToRemove.forEach {
|
||||
val canDeleteRoot = it.root?.stateKey == null
|
||||
if (canDeleteRoot) {
|
||||
it.root?.deleteFromRealm()
|
||||
}
|
||||
it.readReceipts?.readReceipts?.deleteAllFromRealm()
|
||||
it.readReceipts?.deleteFromRealm()
|
||||
it.annotations?.apply {
|
||||
editSummary?.deleteFromRealm()
|
||||
pollResponseSummary?.deleteFromRealm()
|
||||
referencesSummaryEntity?.deleteFromRealm()
|
||||
reactionsSummary.deleteAllFromRealm()
|
||||
}
|
||||
it.annotations?.deleteFromRealm()
|
||||
it.readReceipts?.deleteFromRealm()
|
||||
it.deleteFromRealm()
|
||||
}
|
||||
// We reset the prevToken so we will need to fetch again.
|
||||
chunk.prevToken = null
|
||||
}
|
||||
cleanUp(realm, (threshold / 1.5).toLong())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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.matrix.android.internal.database
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventInsertEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.session.EventInsertLiveProcessor
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmResults
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration,
|
||||
private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>,
|
||||
private val cryptoService: CryptoService)
|
||||
: RealmLiveEntityObserver<EventInsertEntity>(realmConfiguration) {
|
||||
|
||||
override val query = Monarchy.Query<EventInsertEntity> {
|
||||
it.where(EventInsertEntity::class.java)
|
||||
}
|
||||
|
||||
override fun onChange(results: RealmResults<EventInsertEntity>) {
|
||||
if (!results.isLoaded || results.isEmpty()) {
|
||||
return
|
||||
}
|
||||
Timber.v("EventInsertEntity updated with ${results.size} results in db")
|
||||
val filteredEvents = results.mapNotNull {
|
||||
if (shouldProcess(it)) {
|
||||
results.realm.copyFromRealm(it)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
Timber.v("There are ${filteredEvents.size} events to process")
|
||||
observerScope.launch {
|
||||
awaitTransaction(realmConfiguration) { realm ->
|
||||
filteredEvents.forEach { eventInsert ->
|
||||
val eventId = eventInsert.eventId
|
||||
val event = EventEntity.where(realm, eventId).findFirst()
|
||||
if (event == null) {
|
||||
Timber.v("Event $eventId not found")
|
||||
return@forEach
|
||||
}
|
||||
val domainEvent = event.asDomain()
|
||||
decryptIfNeeded(domainEvent)
|
||||
processors.filter {
|
||||
it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType)
|
||||
}.forEach {
|
||||
it.process(realm, domainEvent)
|
||||
}
|
||||
}
|
||||
realm.delete(EventInsertEntity::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun decryptIfNeeded(event: Event) {
|
||||
if (event.isEncrypted() && event.mxDecryptionResult == null) {
|
||||
try {
|
||||
val result = cryptoService.decryptEvent(event, event.roomId ?: "")
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
)
|
||||
} catch (e: MXCryptoError) {
|
||||
Timber.v("Failed to decrypt event")
|
||||
// TODO -> we should keep track of this and retry, or some processing will never be handled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean {
|
||||
return processors.any {
|
||||
it.shouldProcess(eventInsertEntity.eventId, eventInsertEntity.eventType, eventInsertEntity.insertType)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,8 +19,8 @@ package im.vector.matrix.android.internal.database
|
|||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.internal.session.SessionLifecycleObserver
|
||||
import im.vector.matrix.android.internal.util.createBackgroundHandler
|
||||
import io.realm.OrderedRealmCollectionChangeListener
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmChangeListener
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmObject
|
||||
import io.realm.RealmResults
|
||||
|
@ -30,10 +30,10 @@ import kotlinx.coroutines.cancelChildren
|
|||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
internal interface LiveEntityObserver: SessionLifecycleObserver
|
||||
internal interface LiveEntityObserver : SessionLifecycleObserver
|
||||
|
||||
internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val realmConfiguration: RealmConfiguration)
|
||||
: LiveEntityObserver, OrderedRealmCollectionChangeListener<RealmResults<T>> {
|
||||
: LiveEntityObserver, RealmChangeListener<RealmResults<T>> {
|
||||
|
||||
private companion object {
|
||||
val BACKGROUND_HANDLER = createBackgroundHandler("LIVE_ENTITY_BACKGROUND")
|
||||
|
|
|
@ -115,6 +115,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
|
|||
true
|
||||
}
|
||||
}
|
||||
numberOfTimelineEvents++
|
||||
timelineEvents.add(timelineEventEntity)
|
||||
}
|
||||
|
||||
|
@ -122,17 +123,18 @@ private fun computeIsUnique(
|
|||
realm: Realm,
|
||||
roomId: String,
|
||||
isLastForward: Boolean,
|
||||
myRoomMemberContent: RoomMemberContent,
|
||||
senderRoomMemberContent: RoomMemberContent,
|
||||
roomMemberContentsByUser: Map<String, RoomMemberContent?>
|
||||
): Boolean {
|
||||
val isHistoricalUnique = roomMemberContentsByUser.values.find {
|
||||
it != myRoomMemberContent && it?.displayName == myRoomMemberContent.displayName
|
||||
it != senderRoomMemberContent && it?.displayName == senderRoomMemberContent.displayName
|
||||
} == null
|
||||
return if (isLastForward) {
|
||||
val isLiveUnique = RoomMemberSummaryEntity
|
||||
.where(realm, roomId)
|
||||
.equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, myRoomMemberContent.displayName)
|
||||
.findAll().none {
|
||||
.equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, senderRoomMemberContent.displayName)
|
||||
.findAll()
|
||||
.none {
|
||||
!roomMemberContentsByUser.containsKey(it.userId)
|
||||
}
|
||||
isHistoricalUnique && isLiveUnique
|
||||
|
|
|
@ -45,7 +45,6 @@ internal object EventMapper {
|
|||
eventEntity.redacts = event.redacts
|
||||
eventEntity.age = event.unsignedData?.age ?: event.originServerTs
|
||||
eventEntity.unsignedData = uds
|
||||
|
||||
eventEntity.decryptionResultJson = event.mxDecryptionResult?.let {
|
||||
MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java).toJson(it)
|
||||
}
|
||||
|
|
|
@ -1,34 +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.matrix.android.internal.database.mapper
|
||||
|
||||
import im.vector.matrix.android.api.session.group.Group
|
||||
import im.vector.matrix.android.internal.database.model.GroupEntity
|
||||
import im.vector.matrix.android.internal.session.group.DefaultGroup
|
||||
|
||||
internal object GroupMapper {
|
||||
|
||||
fun map(groupEntity: GroupEntity): Group {
|
||||
return DefaultGroup(
|
||||
groupEntity.groupId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun GroupEntity.asDomain(): Group {
|
||||
return GroupMapper.map(this)
|
||||
}
|
|
@ -27,6 +27,7 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
|
|||
@Index var nextToken: String? = null,
|
||||
var stateEvents: RealmList<EventEntity> = RealmList(),
|
||||
var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
|
||||
var numberOfTimelineEvents: Long = 0,
|
||||
// Only one chunk will have isLastForward == true
|
||||
@Index var isLastForward: Boolean = false,
|
||||
@Index var isLastBackward: Boolean = false
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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.matrix.android.internal.database.model
|
||||
|
||||
import io.realm.RealmObject
|
||||
|
||||
/**
|
||||
* This class is used to get notification on new events being inserted. It's to avoid realm getting slow when listening to insert
|
||||
* in EventEntity table.
|
||||
*/
|
||||
internal open class EventInsertEntity(var eventId: String = "",
|
||||
var eventType: String = ""
|
||||
) : RealmObject() {
|
||||
|
||||
private var insertTypeStr: String = EventInsertType.INCREMENTAL_SYNC.name
|
||||
var insertType: EventInsertType
|
||||
get() {
|
||||
return EventInsertType.valueOf(insertTypeStr)
|
||||
}
|
||||
set(value) {
|
||||
insertTypeStr = value.name
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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.matrix.android.internal.database.model;
|
||||
|
||||
public enum EventInsertType {
|
||||
INITIAL_SYNC,
|
||||
INCREMENTAL_SYNC,
|
||||
PAGINATION,
|
||||
LOCAL_ECHO
|
||||
}
|
|
@ -22,8 +22,7 @@ import io.realm.annotations.PrimaryKey
|
|||
|
||||
/**
|
||||
* This class is used to store group info (groupId and membership) from the sync response.
|
||||
* Then [im.vector.matrix.android.internal.session.group.GroupSummaryUpdater] observes change and
|
||||
* makes requests to fetch group information from the homeserver
|
||||
* Then GetGroupDataTask is called regularly to fetch group information from the homeserver.
|
||||
*/
|
||||
internal open class GroupEntity(@PrimaryKey var groupId: String = "")
|
||||
: RealmObject() {
|
||||
|
|
|
@ -24,7 +24,7 @@ import io.realm.annotations.PrimaryKey
|
|||
internal open class RoomMemberSummaryEntity(@PrimaryKey var primaryKey: String = "",
|
||||
@Index var userId: String = "",
|
||||
@Index var roomId: String = "",
|
||||
var displayName: String? = null,
|
||||
@Index var displayName: String? = null,
|
||||
var avatarUrl: String? = null,
|
||||
var reason: String? = null,
|
||||
var isDirect: Boolean = false
|
||||
|
|
|
@ -25,6 +25,7 @@ import io.realm.annotations.RealmModule
|
|||
classes = [
|
||||
ChunkEntity::class,
|
||||
EventEntity::class,
|
||||
EventInsertEntity::class,
|
||||
TimelineEventEntity::class,
|
||||
FilterEntity::class,
|
||||
GroupEntity::class,
|
||||
|
|
|
@ -18,16 +18,28 @@ package im.vector.matrix.android.internal.database.query
|
|||
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.EventInsertEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventInsertType
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmList
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.kotlin.where
|
||||
|
||||
internal fun EventEntity.copyToRealmOrIgnore(realm: Realm): EventEntity {
|
||||
return realm.where<EventEntity>()
|
||||
.equalTo(EventEntityFields.EVENT_ID, eventId)
|
||||
.equalTo(EventEntityFields.ROOM_ID, roomId)
|
||||
.findFirst() ?: realm.copyToRealm(this)
|
||||
internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInsertType): EventEntity {
|
||||
val eventEntity = realm.where<EventEntity>()
|
||||
.equalTo(EventEntityFields.EVENT_ID, eventId)
|
||||
.equalTo(EventEntityFields.ROOM_ID, roomId)
|
||||
.findFirst()
|
||||
return if (eventEntity == null) {
|
||||
val insertEntity = EventInsertEntity(eventId = eventId, eventType = type).apply {
|
||||
this.insertType = insertType
|
||||
}
|
||||
realm.insert(insertEntity)
|
||||
// copy this event entity and return it
|
||||
realm.copyToRealm(this)
|
||||
} else {
|
||||
eventEntity
|
||||
}
|
||||
}
|
||||
|
||||
internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQuery<EventEntity> {
|
||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.database.query
|
|||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.internal.database.model.GroupEntity
|
||||
import im.vector.matrix.android.internal.database.model.GroupEntityFields
|
||||
import im.vector.matrix.android.internal.query.process
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.kotlin.where
|
||||
|
@ -28,10 +29,6 @@ internal fun GroupEntity.Companion.where(realm: Realm, groupId: String): RealmQu
|
|||
.equalTo(GroupEntityFields.GROUP_ID, groupId)
|
||||
}
|
||||
|
||||
internal fun GroupEntity.Companion.where(realm: Realm, membership: Membership? = null): RealmQuery<GroupEntity> {
|
||||
val query = realm.where<GroupEntity>()
|
||||
if (membership != null) {
|
||||
query.equalTo(GroupEntityFields.MEMBERSHIP_STR, membership.name)
|
||||
}
|
||||
return query
|
||||
internal fun GroupEntity.Companion.where(realm: Realm, memberships: List<Membership>): RealmQuery<GroupEntity> {
|
||||
return realm.where<GroupEntity>().process(GroupEntityFields.MEMBERSHIP_STR, memberships)
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.GroupSummaryEntity
|
|||
import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.kotlin.createObject
|
||||
import io.realm.kotlin.where
|
||||
|
||||
internal fun GroupSummaryEntity.Companion.where(realm: Realm, groupId: String? = null): RealmQuery<GroupSummaryEntity> {
|
||||
|
@ -34,3 +35,7 @@ internal fun GroupSummaryEntity.Companion.where(realm: Realm, groupIds: List<Str
|
|||
return realm.where<GroupSummaryEntity>()
|
||||
.`in`(GroupSummaryEntityFields.GROUP_ID, groupIds.toTypedArray())
|
||||
}
|
||||
|
||||
internal fun GroupSummaryEntity.Companion.getOrCreate(realm: Realm, groupId: String): GroupSummaryEntity {
|
||||
return where(realm, groupId).findFirst() ?: realm.createObject(groupId)
|
||||
}
|
||||
|
|
|
@ -21,7 +21,9 @@ import androidx.work.Constraints
|
|||
import androidx.work.ListenableWorker
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class WorkManagerProvider @Inject constructor(
|
||||
|
@ -39,6 +41,14 @@ internal class WorkManagerProvider @Inject constructor(
|
|||
OneTimeWorkRequestBuilder<W>()
|
||||
.addTag(tag)
|
||||
|
||||
/**
|
||||
* Create a PeriodicWorkRequestBuilder, with the Matrix SDK tag
|
||||
*/
|
||||
inline fun <reified W : ListenableWorker> matrixPeriodicWorkRequestBuilder(repeatInterval: Long,
|
||||
repeatIntervalTimeUnit: TimeUnit) =
|
||||
PeriodicWorkRequestBuilder<W>(repeatInterval, repeatIntervalTimeUnit)
|
||||
.addTag(tag)
|
||||
|
||||
/**
|
||||
* Cancel all works instantiated by the Matrix SDK for the current session, and not those from the SDK client, or for other sessions
|
||||
*/
|
||||
|
|
|
@ -50,7 +50,9 @@ import im.vector.matrix.android.api.session.user.UserService
|
|||
import im.vector.matrix.android.api.session.widgets.WidgetService
|
||||
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
||||
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.di.SessionId
|
||||
import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate
|
||||
import im.vector.matrix.android.internal.di.WorkManagerProvider
|
||||
import im.vector.matrix.android.internal.session.identity.DefaultIdentityService
|
||||
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecryptor
|
||||
|
@ -60,8 +62,10 @@ import im.vector.matrix.android.internal.session.sync.job.SyncWorker
|
|||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import im.vector.matrix.android.internal.util.createUIHandler
|
||||
import io.realm.RealmConfiguration
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.OkHttpClient
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
|
@ -76,6 +80,7 @@ internal class DefaultSession @Inject constructor(
|
|||
private val eventBus: EventBus,
|
||||
@SessionId
|
||||
override val sessionId: String,
|
||||
@SessionDatabase private val realmConfiguration: RealmConfiguration,
|
||||
private val lifecycleObservers: Set<@JvmSuppressWildcards SessionLifecycleObserver>,
|
||||
private val sessionListeners: SessionListeners,
|
||||
private val roomService: Lazy<RoomService>,
|
||||
|
@ -110,8 +115,10 @@ internal class DefaultSession @Inject constructor(
|
|||
private val defaultIdentityService: DefaultIdentityService,
|
||||
private val integrationManagerService: IntegrationManagerService,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val callSignalingService: Lazy<CallSignalingService>)
|
||||
: Session,
|
||||
private val callSignalingService: Lazy<CallSignalingService>,
|
||||
@UnauthenticatedWithCertificate
|
||||
private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>
|
||||
) : Session,
|
||||
RoomService by roomService.get(),
|
||||
RoomDirectoryService by roomDirectoryService.get(),
|
||||
GroupService by groupService.get(),
|
||||
|
@ -252,6 +259,10 @@ internal class DefaultSession @Inject constructor(
|
|||
|
||||
override fun callSignalingService(): CallSignalingService = callSignalingService.get()
|
||||
|
||||
override fun getOkHttpClient(): OkHttpClient {
|
||||
return unauthenticatedWithCertificateOkHttpClient.get()
|
||||
}
|
||||
|
||||
override fun addListener(listener: Session.Listener) {
|
||||
sessionListeners.addListener(listener)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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.matrix.android.internal.session
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.internal.database.model.EventInsertType
|
||||
import io.realm.Realm
|
||||
|
||||
internal interface EventInsertLiveProcessor {
|
||||
|
||||
fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean
|
||||
|
||||
suspend fun process(realm: Realm, event: Event)
|
||||
}
|
|
@ -22,7 +22,7 @@ import javax.inject.Inject
|
|||
|
||||
internal class SessionListeners @Inject constructor() {
|
||||
|
||||
private val listeners = ArrayList<Session.Listener>()
|
||||
private val listeners = mutableSetOf<Session.Listener>()
|
||||
|
||||
fun addListener(listener: Session.Listener) {
|
||||
synchronized(listeners) {
|
||||
|
|
|
@ -39,7 +39,9 @@ import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageSer
|
|||
import im.vector.matrix.android.api.session.typing.TypingUsersTracker
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.ShieldTrustUpdater
|
||||
import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecretStorageService
|
||||
import im.vector.matrix.android.internal.crypto.verification.VerificationMessageLiveObserver
|
||||
import im.vector.matrix.android.internal.crypto.verification.VerificationMessageProcessor
|
||||
import im.vector.matrix.android.internal.database.DatabaseCleaner
|
||||
import im.vector.matrix.android.internal.database.EventInsertLiveObserver
|
||||
import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory
|
||||
import im.vector.matrix.android.internal.di.Authenticated
|
||||
import im.vector.matrix.android.internal.di.DeviceId
|
||||
|
@ -64,16 +66,15 @@ import im.vector.matrix.android.internal.network.httpclient.addSocketFactory
|
|||
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
|
||||
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
|
||||
import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider
|
||||
import im.vector.matrix.android.internal.session.call.CallEventObserver
|
||||
import im.vector.matrix.android.internal.session.call.CallEventProcessor
|
||||
import im.vector.matrix.android.internal.session.download.DownloadProgressInterceptor
|
||||
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
|
||||
import im.vector.matrix.android.internal.session.homeserver.DefaultHomeServerCapabilitiesService
|
||||
import im.vector.matrix.android.internal.session.identity.DefaultIdentityService
|
||||
import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManager
|
||||
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
||||
import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLiveObserver
|
||||
import im.vector.matrix.android.internal.session.room.prune.EventsPruner
|
||||
import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventLiveObserver
|
||||
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationProcessor
|
||||
import im.vector.matrix.android.internal.session.room.create.RoomCreateEventProcessor
|
||||
import im.vector.matrix.android.internal.session.room.prune.RedactionEventProcessor
|
||||
import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventProcessor
|
||||
import im.vector.matrix.android.internal.session.securestorage.DefaultSecureStorageService
|
||||
import im.vector.matrix.android.internal.session.typing.DefaultTypingUsersTracker
|
||||
import im.vector.matrix.android.internal.session.user.accountdata.DefaultAccountDataService
|
||||
|
@ -293,31 +294,31 @@ internal abstract class SessionModule {
|
|||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindGroupSummaryUpdater(updater: GroupSummaryUpdater): SessionLifecycleObserver
|
||||
abstract fun bindEventRedactionProcessor(processor: RedactionEventProcessor): EventInsertLiveProcessor
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindEventsPruner(pruner: EventsPruner): SessionLifecycleObserver
|
||||
abstract fun bindEventRelationsAggregationProcessor(processor: EventRelationsAggregationProcessor): EventInsertLiveProcessor
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindEventRelationsAggregationUpdater(updater: EventRelationsAggregationUpdater): SessionLifecycleObserver
|
||||
abstract fun bindRoomTombstoneEventProcessor(processor: RoomTombstoneEventProcessor): EventInsertLiveProcessor
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindRoomTombstoneEventLiveObserver(observer: RoomTombstoneEventLiveObserver): SessionLifecycleObserver
|
||||
abstract fun bindRoomCreateEventProcessor(processor: RoomCreateEventProcessor): EventInsertLiveProcessor
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindRoomCreateEventLiveObserver(observer: RoomCreateEventLiveObserver): SessionLifecycleObserver
|
||||
abstract fun bindVerificationMessageProcessor(processor: VerificationMessageProcessor): EventInsertLiveProcessor
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindVerificationMessageLiveObserver(observer: VerificationMessageLiveObserver): SessionLifecycleObserver
|
||||
abstract fun bindCallEventProcessor(processor: CallEventProcessor): EventInsertLiveProcessor
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindCallEventObserver(observer: CallEventObserver): SessionLifecycleObserver
|
||||
abstract fun bindEventInsertObserver(observer: EventInsertLiveObserver): SessionLifecycleObserver
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
|
@ -335,6 +336,10 @@ internal abstract class SessionModule {
|
|||
@IntoSet
|
||||
abstract fun bindIdentityService(observer: DefaultIdentityService): SessionLifecycleObserver
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindDatabaseCleaner(observer: DatabaseCleaner): SessionLifecycleObserver
|
||||
|
||||
@Binds
|
||||
abstract fun bindInitialSyncProgressService(service: DefaultInitialSyncProgressService): InitialSyncProgressService
|
||||
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.call
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.query.whereTypes
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import io.realm.OrderedCollectionChangeSet
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmResults
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class CallEventObserver @Inject constructor(
|
||||
@SessionDatabase realmConfiguration: RealmConfiguration,
|
||||
@UserId private val userId: String,
|
||||
private val task: CallEventsObserverTask
|
||||
) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
||||
|
||||
override val query = Monarchy.Query<EventEntity> {
|
||||
EventEntity.whereTypes(it, listOf(
|
||||
EventType.CALL_ANSWER,
|
||||
EventType.CALL_CANDIDATES,
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.ENCRYPTED)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
|
||||
Timber.v("EventRelationsAggregationUpdater called with ${changeSet.insertions.size} insertions")
|
||||
|
||||
val insertedDomains = changeSet.insertions
|
||||
.asSequence()
|
||||
.mapNotNull { results[it]?.asDomain() }
|
||||
.toList()
|
||||
|
||||
val params = CallEventsObserverTask.Params(
|
||||
insertedDomains,
|
||||
userId
|
||||
)
|
||||
observerScope.launch {
|
||||
task.execute(params)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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.matrix.android.internal.session.call
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.internal.database.model.EventInsertType
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.session.EventInsertLiveProcessor
|
||||
import io.realm.Realm
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class CallEventProcessor @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
private val callService: DefaultCallSignalingService
|
||||
) : EventInsertLiveProcessor {
|
||||
|
||||
private val allowedTypes = listOf(
|
||||
EventType.CALL_ANSWER,
|
||||
EventType.CALL_CANDIDATES,
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.ENCRYPTED
|
||||
)
|
||||
|
||||
override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
|
||||
if (insertType != EventInsertType.INCREMENTAL_SYNC) {
|
||||
return false
|
||||
}
|
||||
return allowedTypes.contains(eventType)
|
||||
}
|
||||
|
||||
override suspend fun process(realm: Realm, event: Event) {
|
||||
update(realm, event)
|
||||
}
|
||||
|
||||
private fun update(realm: Realm, event: Event) {
|
||||
val now = System.currentTimeMillis()
|
||||
// TODO might check if an invite is not closed (hangup/answsered) in the same event batch?
|
||||
event.roomId ?: return Unit.also {
|
||||
Timber.w("Event with no room id ${event.eventId}")
|
||||
}
|
||||
val age = now - (event.ageLocalTs ?: now)
|
||||
if (age > 40_000) {
|
||||
// To old to ring?
|
||||
return
|
||||
}
|
||||
event.ageLocalTs
|
||||
if (EventType.isCallEvent(event.getClearType())) {
|
||||
callService.onCallEvent(event)
|
||||
}
|
||||
Timber.v("$realm : $userId")
|
||||
}
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.call
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.awaitTransaction
|
||||
import io.realm.Realm
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface CallEventsObserverTask : Task<CallEventsObserverTask.Params, Unit> {
|
||||
|
||||
data class Params(
|
||||
val events: List<Event>,
|
||||
val userId: String
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultCallEventsObserverTask @Inject constructor(
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val cryptoService: CryptoService,
|
||||
private val callService: DefaultCallSignalingService) : CallEventsObserverTask {
|
||||
|
||||
override suspend fun execute(params: CallEventsObserverTask.Params) {
|
||||
val events = params.events
|
||||
val userId = params.userId
|
||||
monarchy.awaitTransaction { realm ->
|
||||
Timber.v(">>> DefaultCallEventsObserverTask[${params.hashCode()}] called with ${events.size} events")
|
||||
update(realm, events, userId)
|
||||
Timber.v("<<< DefaultCallEventsObserverTask[${params.hashCode()}] finished")
|
||||
}
|
||||
}
|
||||
|
||||
private fun update(realm: Realm, events: List<Event>, userId: String) {
|
||||
val now = System.currentTimeMillis()
|
||||
// TODO might check if an invite is not closed (hangup/answsered) in the same event batch?
|
||||
events.forEach { event ->
|
||||
event.roomId ?: return@forEach Unit.also {
|
||||
Timber.w("Event with no room id ${event.eventId}")
|
||||
}
|
||||
val age = now - (event.ageLocalTs ?: now)
|
||||
if (age > 40_000) {
|
||||
// To old to ring?
|
||||
return@forEach
|
||||
}
|
||||
event.ageLocalTs
|
||||
decryptIfNeeded(event)
|
||||
if (EventType.isCallEvent(event.getClearType())) {
|
||||
callService.onCallEvent(event)
|
||||
}
|
||||
}
|
||||
Timber.v("$realm : $userId")
|
||||
}
|
||||
|
||||
private fun decryptIfNeeded(event: Event) {
|
||||
if (event.isEncrypted() && event.mxDecryptionResult == null) {
|
||||
try {
|
||||
val result = cryptoService.decryptEvent(event, event.roomId ?: "")
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
)
|
||||
} catch (e: MXCryptoError) {
|
||||
Timber.v("Call service: Failed to decrypt event")
|
||||
// TODO -> we should keep track of this and retry, or aggregation will be broken
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -41,7 +41,4 @@ internal abstract class CallModule {
|
|||
|
||||
@Binds
|
||||
abstract fun bindGetTurnServerTask(task: DefaultGetTurnServerTask): GetTurnServerTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindCallEventsObserverTask(task: DefaultCallEventsObserverTask): CallEventsObserverTask
|
||||
}
|
||||
|
|
|
@ -18,7 +18,9 @@ package im.vector.matrix.android.internal.session.group
|
|||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.internal.database.model.GroupEntity
|
||||
import im.vector.matrix.android.internal.database.model.GroupSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.query.getOrCreate
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
|
@ -28,11 +30,14 @@ import im.vector.matrix.android.internal.session.group.model.GroupUsers
|
|||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.awaitTransaction
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface GetGroupDataTask : Task<GetGroupDataTask.Params, Unit> {
|
||||
|
||||
data class Params(val groupId: String)
|
||||
sealed class Params {
|
||||
object FetchAllActive : Params()
|
||||
data class FetchWithIds(val groupIds: List<String>) : Params()
|
||||
}
|
||||
}
|
||||
|
||||
internal class DefaultGetGroupDataTask @Inject constructor(
|
||||
|
@ -41,44 +46,64 @@ internal class DefaultGetGroupDataTask @Inject constructor(
|
|||
private val eventBus: EventBus
|
||||
) : GetGroupDataTask {
|
||||
|
||||
private data class GroupData(
|
||||
val groupId: String,
|
||||
val groupSummary: GroupSummaryResponse,
|
||||
val groupRooms: GroupRooms,
|
||||
val groupUsers: GroupUsers
|
||||
)
|
||||
|
||||
override suspend fun execute(params: GetGroupDataTask.Params) {
|
||||
val groupId = params.groupId
|
||||
val groupSummary = executeRequest<GroupSummaryResponse>(eventBus) {
|
||||
apiCall = groupAPI.getSummary(groupId)
|
||||
val groupIds = when (params) {
|
||||
is GetGroupDataTask.Params.FetchAllActive -> {
|
||||
getActiveGroupIds()
|
||||
}
|
||||
is GetGroupDataTask.Params.FetchWithIds -> {
|
||||
params.groupIds
|
||||
}
|
||||
}
|
||||
val groupRooms = executeRequest<GroupRooms>(eventBus) {
|
||||
apiCall = groupAPI.getRooms(groupId)
|
||||
Timber.v("Fetch data for group with ids: ${groupIds.joinToString(";")}")
|
||||
val data = groupIds.map { groupId ->
|
||||
val groupSummary = executeRequest<GroupSummaryResponse>(eventBus) {
|
||||
apiCall = groupAPI.getSummary(groupId)
|
||||
}
|
||||
val groupRooms = executeRequest<GroupRooms>(eventBus) {
|
||||
apiCall = groupAPI.getRooms(groupId)
|
||||
}
|
||||
val groupUsers = executeRequest<GroupUsers>(eventBus) {
|
||||
apiCall = groupAPI.getUsers(groupId)
|
||||
}
|
||||
GroupData(groupId, groupSummary, groupRooms, groupUsers)
|
||||
}
|
||||
val groupUsers = executeRequest<GroupUsers>(eventBus) {
|
||||
apiCall = groupAPI.getUsers(groupId)
|
||||
}
|
||||
insertInDb(groupSummary, groupRooms, groupUsers, groupId)
|
||||
insertInDb(data)
|
||||
}
|
||||
|
||||
private suspend fun insertInDb(groupSummary: GroupSummaryResponse,
|
||||
groupRooms: GroupRooms,
|
||||
groupUsers: GroupUsers,
|
||||
groupId: String) {
|
||||
private fun getActiveGroupIds(): List<String> {
|
||||
return monarchy.fetchAllMappedSync(
|
||||
{ realm ->
|
||||
GroupEntity.where(realm, Membership.activeMemberships())
|
||||
},
|
||||
{ it.groupId }
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun insertInDb(groupDataList: List<GroupData>) {
|
||||
monarchy
|
||||
.awaitTransaction { realm ->
|
||||
val groupSummaryEntity = GroupSummaryEntity.where(realm, groupId).findFirst()
|
||||
?: realm.createObject(GroupSummaryEntity::class.java, groupId)
|
||||
groupDataList.forEach { groupData ->
|
||||
|
||||
groupSummaryEntity.avatarUrl = groupSummary.profile?.avatarUrl ?: ""
|
||||
val name = groupSummary.profile?.name
|
||||
groupSummaryEntity.displayName = if (name.isNullOrEmpty()) groupId else name
|
||||
groupSummaryEntity.shortDescription = groupSummary.profile?.shortDescription ?: ""
|
||||
val groupSummaryEntity = GroupSummaryEntity.getOrCreate(realm, groupData.groupId)
|
||||
|
||||
groupSummaryEntity.roomIds.clear()
|
||||
groupRooms.rooms.mapTo(groupSummaryEntity.roomIds) { it.roomId }
|
||||
groupSummaryEntity.avatarUrl = groupData.groupSummary.profile?.avatarUrl ?: ""
|
||||
val name = groupData.groupSummary.profile?.name
|
||||
groupSummaryEntity.displayName = if (name.isNullOrEmpty()) groupData.groupId else name
|
||||
groupSummaryEntity.shortDescription = groupData.groupSummary.profile?.shortDescription ?: ""
|
||||
|
||||
groupSummaryEntity.userIds.clear()
|
||||
groupUsers.users.mapTo(groupSummaryEntity.userIds) { it.userId }
|
||||
groupSummaryEntity.roomIds.clear()
|
||||
groupData.groupRooms.rooms.mapTo(groupSummaryEntity.roomIds) { it.roomId }
|
||||
|
||||
groupSummaryEntity.membership = when (groupSummary.user?.membership) {
|
||||
Membership.JOIN.value -> Membership.JOIN
|
||||
Membership.INVITE.value -> Membership.INVITE
|
||||
else -> Membership.LEAVE
|
||||
groupSummaryEntity.userIds.clear()
|
||||
groupData.groupUsers.users.mapTo(groupSummaryEntity.userIds) { it.userId }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,20 @@
|
|||
|
||||
package im.vector.matrix.android.internal.session.group
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.group.Group
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
|
||||
internal class DefaultGroup(override val groupId: String) : Group
|
||||
internal class DefaultGroup(override val groupId: String,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val getGroupDataTask: GetGroupDataTask) : Group {
|
||||
|
||||
override fun fetchGroupData(callback: MatrixCallback<Unit>): Cancelable {
|
||||
val params = GetGroupDataTask.Params.FetchWithIds(listOf(groupId))
|
||||
return getGroupDataTask.configureWith(params) {
|
||||
this.callback = callback
|
||||
}.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.group.GroupService
|
|||
import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.GroupEntity
|
||||
import im.vector.matrix.android.internal.database.model.GroupSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
|
@ -33,10 +34,15 @@ import io.realm.Realm
|
|||
import io.realm.RealmQuery
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultGroupService @Inject constructor(@SessionDatabase private val monarchy: Monarchy) : GroupService {
|
||||
internal class DefaultGroupService @Inject constructor(@SessionDatabase private val monarchy: Monarchy,
|
||||
private val groupFactory: GroupFactory) : GroupService {
|
||||
|
||||
override fun getGroup(groupId: String): Group? {
|
||||
return null
|
||||
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
||||
GroupEntity.where(realm, groupId).findFirst()?.let {
|
||||
groupFactory.create(groupId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getGroupSummary(groupId: String): GroupSummary? {
|
||||
|
|
|
@ -35,7 +35,6 @@ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) :
|
|||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
override val sessionId: String,
|
||||
val groupIds: List<String>,
|
||||
override val lastFailureMessage: String? = null
|
||||
) : SessionWorkerParams
|
||||
|
||||
|
@ -48,14 +47,11 @@ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) :
|
|||
|
||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||
sessionComponent.inject(this)
|
||||
val results = params.groupIds.map { groupId ->
|
||||
runCatching { fetchGroupData(groupId) }
|
||||
}
|
||||
val isSuccessful = results.none { it.isFailure }
|
||||
return if (isSuccessful) Result.success() else Result.retry()
|
||||
}
|
||||
|
||||
private suspend fun fetchGroupData(groupId: String) {
|
||||
getGroupDataTask.execute(GetGroupDataTask.Params(groupId))
|
||||
return runCatching {
|
||||
getGroupDataTask.execute(GetGroupDataTask.Params.FetchAllActive)
|
||||
}.fold(
|
||||
{ Result.success() },
|
||||
{ Result.retry() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.matrix.android.internal.session.group
|
||||
|
||||
import im.vector.matrix.android.api.session.group.Group
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface GroupFactory {
|
||||
fun create(groupId: String): Group
|
||||
}
|
||||
|
||||
@SessionScope
|
||||
internal class DefaultGroupFactory @Inject constructor(private val getGroupDataTask: GetGroupDataTask,
|
||||
private val taskExecutor: TaskExecutor) :
|
||||
GroupFactory {
|
||||
|
||||
override fun create(groupId: String): Group {
|
||||
return DefaultGroup(
|
||||
groupId = groupId,
|
||||
taskExecutor = taskExecutor,
|
||||
getGroupDataTask = getGroupDataTask
|
||||
)
|
||||
}
|
||||
}
|
|
@ -36,6 +36,9 @@ internal abstract class GroupModule {
|
|||
}
|
||||
}
|
||||
|
||||
@Binds
|
||||
abstract fun bindGroupFactory(factory: DefaultGroupFactory): GroupFactory
|
||||
|
||||
@Binds
|
||||
abstract fun bindGetGroupDataTask(task: DefaultGetGroupDataTask): GetGroupDataTask
|
||||
|
||||
|
|
|
@ -1,91 +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.matrix.android.internal.session.group
|
||||
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
||||
import im.vector.matrix.android.internal.database.awaitTransaction
|
||||
import im.vector.matrix.android.internal.database.model.GroupEntity
|
||||
import im.vector.matrix.android.internal.database.model.GroupSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.di.SessionId
|
||||
import im.vector.matrix.android.internal.di.WorkManagerProvider
|
||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||
import io.realm.OrderedCollectionChangeSet
|
||||
import io.realm.RealmResults
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER"
|
||||
|
||||
internal class GroupSummaryUpdater @Inject constructor(
|
||||
private val workManagerProvider: WorkManagerProvider,
|
||||
@SessionId private val sessionId: String,
|
||||
@SessionDatabase private val monarchy: Monarchy)
|
||||
: RealmLiveEntityObserver<GroupEntity>(monarchy.realmConfiguration) {
|
||||
|
||||
override val query = Monarchy.Query { GroupEntity.where(it) }
|
||||
|
||||
override fun onChange(results: RealmResults<GroupEntity>, changeSet: OrderedCollectionChangeSet) {
|
||||
// `insertions` for new groups and `changes` to handle left groups
|
||||
val modifiedGroupEntity = (changeSet.insertions + changeSet.changes)
|
||||
.asSequence()
|
||||
.mapNotNull { results[it] }
|
||||
|
||||
fetchGroupsData(modifiedGroupEntity
|
||||
.filter { it.membership == Membership.JOIN || it.membership == Membership.INVITE }
|
||||
.map { it.groupId }
|
||||
.toList())
|
||||
|
||||
modifiedGroupEntity
|
||||
.filter { it.membership == Membership.LEAVE }
|
||||
.map { it.groupId }
|
||||
.toList()
|
||||
.also {
|
||||
observerScope.launch {
|
||||
deleteGroups(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchGroupsData(groupIds: List<String>) {
|
||||
val getGroupDataWorkerParams = GetGroupDataWorker.Params(sessionId, groupIds)
|
||||
|
||||
val workData = WorkerParamsFactory.toData(getGroupDataWorkerParams)
|
||||
|
||||
val getGroupWork = workManagerProvider.matrixOneTimeWorkRequestBuilder<GetGroupDataWorker>()
|
||||
.setInputData(workData)
|
||||
.setConstraints(WorkManagerProvider.workConstraints)
|
||||
.build()
|
||||
|
||||
workManagerProvider.workManager
|
||||
.beginUniqueWork(GET_GROUP_DATA_WORKER, ExistingWorkPolicy.APPEND, getGroupWork)
|
||||
.enqueue()
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the GroupSummaryEntity of left groups
|
||||
*/
|
||||
private suspend fun deleteGroups(groupIds: List<String>) = awaitTransaction(monarchy.realmConfiguration) { realm ->
|
||||
GroupSummaryEntity.where(realm, groupIds)
|
||||
.findAll()
|
||||
.deleteAllFromRealm()
|
||||
}
|
||||
}
|
|
@ -62,6 +62,7 @@ import javax.net.ssl.HttpsURLConnection
|
|||
@SessionScope
|
||||
internal class DefaultIdentityService @Inject constructor(
|
||||
private val identityStore: IdentityStore,
|
||||
private val ensureIdentityTokenTask: EnsureIdentityTokenTask,
|
||||
private val getOpenIdTokenTask: GetOpenIdTokenTask,
|
||||
private val identityBulkLookupTask: IdentityBulkLookupTask,
|
||||
private val identityRegisterTask: IdentityRegisterTask,
|
||||
|
@ -278,7 +279,7 @@ internal class DefaultIdentityService @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun lookUpInternal(canRetry: Boolean, threePids: List<ThreePid>): List<FoundThreePid> {
|
||||
ensureToken()
|
||||
ensureIdentityTokenTask.execute(Unit)
|
||||
|
||||
return try {
|
||||
identityBulkLookupTask.execute(IdentityBulkLookupTask.Params(threePids))
|
||||
|
@ -295,17 +296,6 @@ internal class DefaultIdentityService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun ensureToken() {
|
||||
val identityData = identityStore.getIdentityData() ?: throw IdentityServiceError.NoIdentityServerConfigured
|
||||
val url = identityData.identityServerUrl ?: throw IdentityServiceError.NoIdentityServerConfigured
|
||||
|
||||
if (identityData.token == null) {
|
||||
// Try to get a token
|
||||
val token = getNewIdentityServerToken(url)
|
||||
identityStore.setToken(token)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getNewIdentityServerToken(url: String): String {
|
||||
val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java)
|
||||
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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.matrix.android.internal.session.identity
|
||||
|
||||
import dagger.Lazy
|
||||
import im.vector.matrix.android.api.session.identity.IdentityServiceError
|
||||
import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate
|
||||
import im.vector.matrix.android.internal.network.RetrofitFactory
|
||||
import im.vector.matrix.android.internal.session.identity.data.IdentityStore
|
||||
import im.vector.matrix.android.internal.session.openid.GetOpenIdTokenTask
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface EnsureIdentityTokenTask : Task<Unit, Unit>
|
||||
|
||||
internal class DefaultEnsureIdentityTokenTask @Inject constructor(
|
||||
private val identityStore: IdentityStore,
|
||||
private val retrofitFactory: RetrofitFactory,
|
||||
@UnauthenticatedWithCertificate
|
||||
private val unauthenticatedOkHttpClient: Lazy<OkHttpClient>,
|
||||
private val getOpenIdTokenTask: GetOpenIdTokenTask,
|
||||
private val identityRegisterTask: IdentityRegisterTask
|
||||
) : EnsureIdentityTokenTask {
|
||||
|
||||
override suspend fun execute(params: Unit) {
|
||||
val identityData = identityStore.getIdentityData() ?: throw IdentityServiceError.NoIdentityServerConfigured
|
||||
val url = identityData.identityServerUrl ?: throw IdentityServiceError.NoIdentityServerConfigured
|
||||
|
||||
if (identityData.token == null) {
|
||||
// Try to get a token
|
||||
val token = getNewIdentityServerToken(url)
|
||||
identityStore.setToken(token)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getNewIdentityServerToken(url: String): String {
|
||||
val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java)
|
||||
|
||||
val openIdToken = getOpenIdTokenTask.execute(Unit)
|
||||
val token = identityRegisterTask.execute(IdentityRegisterTask.Params(api, openIdToken))
|
||||
|
||||
return token.token
|
||||
}
|
||||
}
|
|
@ -78,6 +78,9 @@ internal abstract class IdentityModule {
|
|||
@Binds
|
||||
abstract fun bindIdentityStore(store: RealmIdentityStore): IdentityStore
|
||||
|
||||
@Binds
|
||||
abstract fun bindEnsureIdentityTokenTask(task: DefaultEnsureIdentityTokenTask): EnsureIdentityTokenTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindIdentityPingTask(task: DefaultIdentityPingTask): IdentityPingTask
|
||||
|
||||
|
|
|
@ -24,13 +24,11 @@ import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProt
|
|||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.session.room.directory.GetPublicRoomTask
|
||||
import im.vector.matrix.android.internal.session.room.directory.GetThirdPartyProtocolsTask
|
||||
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultRoomDirectoryService @Inject constructor(private val getPublicRoomTask: GetPublicRoomTask,
|
||||
private val joinRoomTask: JoinRoomTask,
|
||||
private val getThirdPartyProtocolsTask: GetThirdPartyProtocolsTask,
|
||||
private val taskExecutor: TaskExecutor) : RoomDirectoryService {
|
||||
|
||||
|
@ -44,14 +42,6 @@ internal class DefaultRoomDirectoryService @Inject constructor(private val getPu
|
|||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun joinRoom(roomIdOrAlias: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {
|
||||
return joinRoomTask
|
||||
.configureWith(JoinRoomTask.Params(roomIdOrAlias, reason)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>): Cancelable {
|
||||
return getThirdPartyProtocolsTask
|
||||
.configureWith {
|
||||
|
|
|
@ -21,12 +21,14 @@ import im.vector.matrix.android.api.MatrixCallback
|
|||
import im.vector.matrix.android.api.session.room.Room
|
||||
import im.vector.matrix.android.api.session.room.RoomService
|
||||
import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
|
||||
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
|
||||
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
|
||||
import im.vector.matrix.android.internal.session.room.membership.RoomChangeMembershipStateDataSource
|
||||
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
|
||||
import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
|
||||
import im.vector.matrix.android.internal.session.room.summary.RoomSummaryDataSource
|
||||
|
@ -43,6 +45,7 @@ internal class DefaultRoomService @Inject constructor(
|
|||
private val roomIdByAliasTask: GetRoomIdByAliasTask,
|
||||
private val roomGetter: RoomGetter,
|
||||
private val roomSummaryDataSource: RoomSummaryDataSource,
|
||||
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
|
||||
private val taskExecutor: TaskExecutor
|
||||
) : RoomService {
|
||||
|
||||
|
@ -111,4 +114,8 @@ internal class DefaultRoomService @Inject constructor(
|
|||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>> {
|
||||
return roomChangeMembershipStateDataSource.getLiveStates()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,9 +15,7 @@
|
|||
*/
|
||||
package im.vector.matrix.android.internal.session.room
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.events.model.AggregatedAnnotation
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
|
@ -32,13 +30,13 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
|||
import im.vector.matrix.android.api.session.room.model.message.MessagePollResponseContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent
|
||||
import im.vector.matrix.android.api.session.room.model.relation.ReactionContent
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
||||
import im.vector.matrix.android.internal.database.mapper.EventMapper
|
||||
import im.vector.matrix.android.internal.database.model.EditAggregatedSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventInsertType
|
||||
import im.vector.matrix.android.internal.database.model.PollResponseAggregatedSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields
|
||||
|
@ -47,21 +45,12 @@ import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
|||
import im.vector.matrix.android.internal.database.query.create
|
||||
import im.vector.matrix.android.internal.database.query.getOrCreate
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.awaitTransaction
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.session.EventInsertLiveProcessor
|
||||
import io.realm.Realm
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface EventRelationsAggregationTask : Task<EventRelationsAggregationTask.Params, Unit> {
|
||||
|
||||
data class Params(
|
||||
val events: List<Event>,
|
||||
val userId: String
|
||||
)
|
||||
}
|
||||
|
||||
enum class VerificationState {
|
||||
REQUEST,
|
||||
WAITING,
|
||||
|
@ -89,161 +78,145 @@ private fun VerificationState?.toState(newState: VerificationState): Verificatio
|
|||
return newState
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base.
|
||||
*/
|
||||
internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val cryptoService: CryptoService) : EventRelationsAggregationTask {
|
||||
internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String,
|
||||
private val cryptoService: CryptoService
|
||||
) : EventInsertLiveProcessor {
|
||||
|
||||
// OPT OUT serer aggregation until API mature enough
|
||||
private val SHOULD_HANDLE_SERVER_AGREGGATION = false
|
||||
private val allowedTypes = listOf(
|
||||
EventType.MESSAGE,
|
||||
EventType.REDACTION,
|
||||
EventType.REACTION,
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
// TODO Add ?
|
||||
// EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.ENCRYPTED
|
||||
)
|
||||
|
||||
override suspend fun execute(params: EventRelationsAggregationTask.Params) {
|
||||
val events = params.events
|
||||
val userId = params.userId
|
||||
monarchy.awaitTransaction { realm ->
|
||||
Timber.v(">>> DefaultEventRelationsAggregationTask[${params.hashCode()}] called with ${events.size} events")
|
||||
update(realm, events, userId)
|
||||
Timber.v("<<< DefaultEventRelationsAggregationTask[${params.hashCode()}] finished")
|
||||
}
|
||||
override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
|
||||
return allowedTypes.contains(eventType)
|
||||
}
|
||||
|
||||
private fun update(realm: Realm, events: List<Event>, userId: String) {
|
||||
events.forEach { event ->
|
||||
try { // Temporary catch, should be removed
|
||||
val roomId = event.roomId
|
||||
if (roomId == null) {
|
||||
Timber.w("Event has no room id ${event.eventId}")
|
||||
return@forEach
|
||||
override suspend fun process(realm: Realm, event: Event) {
|
||||
try { // Temporary catch, should be removed
|
||||
val roomId = event.roomId
|
||||
if (roomId == null) {
|
||||
Timber.w("Event has no room id ${event.eventId}")
|
||||
return
|
||||
}
|
||||
val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "")
|
||||
when (event.type) {
|
||||
EventType.REACTION -> {
|
||||
// we got a reaction!!
|
||||
Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}")
|
||||
handleReaction(event, roomId, realm, userId, isLocalEcho)
|
||||
}
|
||||
val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "")
|
||||
when (event.type) {
|
||||
EventType.REACTION -> {
|
||||
// we got a reaction!!
|
||||
Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}")
|
||||
handleReaction(event, roomId, realm, userId, isLocalEcho)
|
||||
}
|
||||
EventType.MESSAGE -> {
|
||||
if (event.unsignedData?.relations?.annotations != null) {
|
||||
Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}")
|
||||
handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm)
|
||||
EventType.MESSAGE -> {
|
||||
if (event.unsignedData?.relations?.annotations != null) {
|
||||
Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}")
|
||||
handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm)
|
||||
|
||||
EventAnnotationsSummaryEntity.where(realm, event.eventId
|
||||
?: "").findFirst()?.let {
|
||||
TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId
|
||||
?: "").findFirst()?.let { tet ->
|
||||
tet.annotations = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val content: MessageContent? = event.content.toModel()
|
||||
if (content?.relatesTo?.type == RelationType.REPLACE) {
|
||||
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
|
||||
// A replace!
|
||||
handleReplace(realm, event, content, roomId, isLocalEcho)
|
||||
} else if (content?.relatesTo?.type == RelationType.RESPONSE) {
|
||||
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
|
||||
handleResponse(realm, userId, event, content, roomId, isLocalEcho)
|
||||
}
|
||||
}
|
||||
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_KEY -> {
|
||||
Timber.v("## SAS REF in room $roomId for event ${event.eventId}")
|
||||
event.content.toModel<MessageRelationContent>()?.relatesTo?.let {
|
||||
if (it.type == RelationType.REFERENCE && it.eventId != null) {
|
||||
handleVerification(realm, event, roomId, isLocalEcho, it.eventId, userId)
|
||||
EventAnnotationsSummaryEntity.where(realm, event.eventId
|
||||
?: "").findFirst()?.let {
|
||||
TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId
|
||||
?: "").findFirst()?.let { tet ->
|
||||
tet.annotations = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EventType.ENCRYPTED -> {
|
||||
// Relation type is in clear
|
||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
||||
if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE
|
||||
|| encryptedEventContent?.relatesTo?.type == RelationType.RESPONSE
|
||||
) {
|
||||
// we need to decrypt if needed
|
||||
decryptIfNeeded(event)
|
||||
event.getClearContent().toModel<MessageContent>()?.let {
|
||||
if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) {
|
||||
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
|
||||
// A replace!
|
||||
handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
||||
} else if (encryptedEventContent.relatesTo.type == RelationType.RESPONSE) {
|
||||
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
|
||||
handleResponse(realm, userId, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
||||
}
|
||||
val content: MessageContent? = event.content.toModel()
|
||||
if (content?.relatesTo?.type == RelationType.REPLACE) {
|
||||
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
|
||||
// A replace!
|
||||
handleReplace(realm, event, content, roomId, isLocalEcho)
|
||||
} else if (content?.relatesTo?.type == RelationType.RESPONSE) {
|
||||
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
|
||||
handleResponse(realm, userId, event, content, roomId, isLocalEcho)
|
||||
}
|
||||
}
|
||||
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_KEY -> {
|
||||
Timber.v("## SAS REF in room $roomId for event ${event.eventId}")
|
||||
event.content.toModel<MessageRelationContent>()?.relatesTo?.let {
|
||||
if (it.type == RelationType.REFERENCE && it.eventId != null) {
|
||||
handleVerification(realm, event, roomId, isLocalEcho, it.eventId, userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EventType.ENCRYPTED -> {
|
||||
// Relation type is in clear
|
||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
||||
if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE
|
||||
|| encryptedEventContent?.relatesTo?.type == RelationType.RESPONSE
|
||||
) {
|
||||
event.getClearContent().toModel<MessageContent>()?.let {
|
||||
if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) {
|
||||
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
|
||||
// A replace!
|
||||
handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
||||
} else if (encryptedEventContent.relatesTo.type == RelationType.RESPONSE) {
|
||||
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
|
||||
handleResponse(realm, userId, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
||||
}
|
||||
} else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) {
|
||||
decryptIfNeeded(event)
|
||||
when (event.getClearType()) {
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_KEY -> {
|
||||
Timber.v("## SAS REF in room $roomId for event ${event.eventId}")
|
||||
encryptedEventContent.relatesTo.eventId?.let {
|
||||
handleVerification(realm, event, roomId, isLocalEcho, it, userId)
|
||||
}
|
||||
}
|
||||
} else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) {
|
||||
when (event.getClearType()) {
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_KEY -> {
|
||||
Timber.v("## SAS REF in room $roomId for event ${event.eventId}")
|
||||
encryptedEventContent.relatesTo.eventId?.let {
|
||||
handleVerification(realm, event, roomId, isLocalEcho, it, userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EventType.REDACTION -> {
|
||||
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
|
||||
?: return@forEach
|
||||
when (eventToPrune.type) {
|
||||
EventType.MESSAGE -> {
|
||||
Timber.d("REDACTION for message ${eventToPrune.eventId}")
|
||||
}
|
||||
EventType.REDACTION -> {
|
||||
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
|
||||
?: return
|
||||
when (eventToPrune.type) {
|
||||
EventType.MESSAGE -> {
|
||||
Timber.d("REDACTION for message ${eventToPrune.eventId}")
|
||||
// val unsignedData = EventMapper.map(eventToPrune).unsignedData
|
||||
// ?: UnsignedData(null, null)
|
||||
|
||||
// was this event a m.replace
|
||||
val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
|
||||
if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
|
||||
handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
|
||||
}
|
||||
}
|
||||
EventType.REACTION -> {
|
||||
handleReactionRedact(eventToPrune, realm, userId)
|
||||
// was this event a m.replace
|
||||
val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
|
||||
if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
|
||||
handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
|
||||
}
|
||||
}
|
||||
EventType.REACTION -> {
|
||||
handleReactionRedact(eventToPrune, realm, userId)
|
||||
}
|
||||
}
|
||||
else -> Timber.v("UnHandled event ${event.eventId}")
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Timber.e(t, "## Should not happen ")
|
||||
else -> Timber.v("UnHandled event ${event.eventId}")
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Timber.e(t, "## Should not happen ")
|
||||
}
|
||||
}
|
||||
|
||||
private fun decryptIfNeeded(event: Event) {
|
||||
if (event.mxDecryptionResult == null) {
|
||||
try {
|
||||
val result = cryptoService.decryptEvent(event, "")
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
)
|
||||
} catch (e: MXCryptoError) {
|
||||
Timber.v("Failed to decrypt e2e replace")
|
||||
// TODO -> we should keep track of this and retry, or aggregation will be broken
|
||||
}
|
||||
}
|
||||
}
|
||||
// OPT OUT serer aggregation until API mature enough
|
||||
private val SHOULD_HANDLE_SERVER_AGREGGATION = false
|
||||
|
||||
private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean, relatedEventId: String? = null) {
|
||||
val eventId = event.eventId ?: return
|
|
@ -1,76 +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.matrix.android.internal.session.room
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.query.whereTypes
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import io.realm.OrderedCollectionChangeSet
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmResults
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Acts as a listener of incoming messages in order to incrementally computes a summary of annotations.
|
||||
* For reactions will build a EventAnnotationsSummaryEntity, ans for edits a EditAggregatedSummaryEntity.
|
||||
* The summaries can then be extracted and added (as a decoration) to a TimelineEvent for final display.
|
||||
*/
|
||||
internal class EventRelationsAggregationUpdater @Inject constructor(
|
||||
@SessionDatabase realmConfiguration: RealmConfiguration,
|
||||
@UserId private val userId: String,
|
||||
private val task: EventRelationsAggregationTask) :
|
||||
RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
||||
|
||||
override val query = Monarchy.Query<EventEntity> {
|
||||
EventEntity.whereTypes(it, listOf(
|
||||
EventType.MESSAGE,
|
||||
EventType.REDACTION,
|
||||
EventType.REACTION,
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
// TODO Add ?
|
||||
// EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.ENCRYPTED)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
|
||||
Timber.v("EventRelationsAggregationUpdater called with ${changeSet.insertions.size} insertions")
|
||||
|
||||
val insertedDomains = changeSet.insertions
|
||||
.asSequence()
|
||||
.mapNotNull { results[it]?.asDomain() }
|
||||
.toList()
|
||||
val params = EventRelationsAggregationTask.Params(
|
||||
insertedDomains,
|
||||
userId
|
||||
)
|
||||
observerScope.launch {
|
||||
task.execute(params)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,9 +18,6 @@ package im.vector.matrix.android.internal.session.room
|
|||
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
|
||||
import im.vector.matrix.android.api.session.room.model.create.JoinRoomResponse
|
||||
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams
|
||||
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse
|
||||
import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol
|
||||
|
@ -28,9 +25,13 @@ import im.vector.matrix.android.api.util.JsonDict
|
|||
import im.vector.matrix.android.internal.network.NetworkConstants
|
||||
import im.vector.matrix.android.internal.session.room.alias.AddRoomAliasBody
|
||||
import im.vector.matrix.android.internal.session.room.alias.RoomAliasDescription
|
||||
import im.vector.matrix.android.internal.session.room.create.CreateRoomBody
|
||||
import im.vector.matrix.android.internal.session.room.create.CreateRoomResponse
|
||||
import im.vector.matrix.android.internal.session.room.create.JoinRoomResponse
|
||||
import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse
|
||||
import im.vector.matrix.android.internal.session.room.membership.admin.UserIdAndReason
|
||||
import im.vector.matrix.android.internal.session.room.membership.joining.InviteBody
|
||||
import im.vector.matrix.android.internal.session.room.membership.threepid.ThreePidInviteBody
|
||||
import im.vector.matrix.android.internal.session.room.relation.RelationsResponse
|
||||
import im.vector.matrix.android.internal.session.room.reporting.ReportContentBody
|
||||
import im.vector.matrix.android.internal.session.room.send.SendResponse
|
||||
|
@ -79,7 +80,7 @@ internal interface RoomAPI {
|
|||
*/
|
||||
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "createRoom")
|
||||
fun createRoom(@Body param: CreateRoomParams): Call<CreateRoomResponse>
|
||||
fun createRoom(@Body param: CreateRoomBody): Call<CreateRoomResponse>
|
||||
|
||||
/**
|
||||
* Get a list of messages starting from a reference.
|
||||
|
@ -170,6 +171,14 @@ internal interface RoomAPI {
|
|||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite")
|
||||
fun invite(@Path("roomId") roomId: String, @Body body: InviteBody): Call<Unit>
|
||||
|
||||
/**
|
||||
* Invite a user to a room, using a ThreePid
|
||||
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#id101
|
||||
* @param roomId Required. The room identifier (not alias) to which to invite the user.
|
||||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite")
|
||||
fun invite3pid(@Path("roomId") roomId: String, @Body body: ThreePidInviteBody): Call<Unit>
|
||||
|
||||
/**
|
||||
* Send a generic state events
|
||||
*
|
||||
|
|
|
@ -44,8 +44,8 @@ import im.vector.matrix.android.internal.session.room.membership.joining.InviteT
|
|||
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
|
||||
import im.vector.matrix.android.internal.session.room.membership.leaving.DefaultLeaveRoomTask
|
||||
import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask
|
||||
import im.vector.matrix.android.internal.session.room.prune.DefaultPruneEventTask
|
||||
import im.vector.matrix.android.internal.session.room.prune.PruneEventTask
|
||||
import im.vector.matrix.android.internal.session.room.membership.threepid.DefaultInviteThreePidTask
|
||||
import im.vector.matrix.android.internal.session.room.membership.threepid.InviteThreePidTask
|
||||
import im.vector.matrix.android.internal.session.room.read.DefaultMarkAllRoomsReadTask
|
||||
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
|
||||
import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
|
||||
|
@ -64,10 +64,10 @@ import im.vector.matrix.android.internal.session.room.tags.AddTagToRoomTask
|
|||
import im.vector.matrix.android.internal.session.room.tags.DefaultAddTagToRoomTask
|
||||
import im.vector.matrix.android.internal.session.room.tags.DefaultDeleteTagFromRoomTask
|
||||
import im.vector.matrix.android.internal.session.room.tags.DeleteTagFromRoomTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultFetchNextTokenAndPaginateTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultFetchTokenAndPaginateTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.FetchNextTokenAndPaginateTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.FetchTokenAndPaginateTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
|
||||
import im.vector.matrix.android.internal.session.room.typing.DefaultSendTypingTask
|
||||
|
@ -129,9 +129,6 @@ internal abstract class RoomModule {
|
|||
@Binds
|
||||
abstract fun bindFileService(service: DefaultFileService): FileService
|
||||
|
||||
@Binds
|
||||
abstract fun bindEventRelationsAggregationTask(task: DefaultEventRelationsAggregationTask): EventRelationsAggregationTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindCreateRoomTask(task: DefaultCreateRoomTask): CreateRoomTask
|
||||
|
||||
|
@ -144,6 +141,9 @@ internal abstract class RoomModule {
|
|||
@Binds
|
||||
abstract fun bindInviteTask(task: DefaultInviteTask): InviteTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindInviteThreePidTask(task: DefaultInviteThreePidTask): InviteThreePidTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindJoinRoomTask(task: DefaultJoinRoomTask): JoinRoomTask
|
||||
|
||||
|
@ -156,9 +156,6 @@ internal abstract class RoomModule {
|
|||
@Binds
|
||||
abstract fun bindLoadRoomMembersTask(task: DefaultLoadRoomMembersTask): LoadRoomMembersTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindPruneEventTask(task: DefaultPruneEventTask): PruneEventTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSetReadMarkersTask(task: DefaultSetReadMarkersTask): SetReadMarkersTask
|
||||
|
||||
|
@ -184,7 +181,7 @@ internal abstract class RoomModule {
|
|||
abstract fun bindPaginationTask(task: DefaultPaginationTask): PaginationTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindFetchNextTokenAndPaginateTask(task: DefaultFetchNextTokenAndPaginateTask): FetchNextTokenAndPaginateTask
|
||||
abstract fun bindFetchNextTokenAndPaginateTask(task: DefaultFetchTokenAndPaginateTask): FetchTokenAndPaginateTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindFetchEditHistoryTask(task: DefaultFetchEditHistoryTask): FetchEditHistoryTask
|
||||
|
|
|
@ -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.matrix.android.internal.session.room.create
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
|
||||
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomPreset
|
||||
import im.vector.matrix.android.internal.session.room.membership.threepid.ThreePidInviteBody
|
||||
|
||||
/**
|
||||
* Parameter to create a room
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class CreateRoomBody(
|
||||
/**
|
||||
* A public visibility indicates that the room will be shown in the published room list.
|
||||
* A private visibility will hide the room from the published room list.
|
||||
* Rooms default to private visibility if this key is not included.
|
||||
* NB: This should not be confused with join_rules which also uses the word public. One of: ["public", "private"]
|
||||
*/
|
||||
@Json(name = "visibility")
|
||||
val visibility: RoomDirectoryVisibility?,
|
||||
|
||||
/**
|
||||
* The desired room alias local part. If this is included, a room alias will be created and mapped to the newly created room.
|
||||
* The alias will belong on the same homeserver which created the room.
|
||||
* For example, if this was set to "foo" and sent to the homeserver "example.com" the complete room alias would be #foo:example.com.
|
||||
*/
|
||||
@Json(name = "room_alias_name")
|
||||
val roomAliasName: String?,
|
||||
|
||||
/**
|
||||
* If this is included, an m.room.name event will be sent into the room to indicate the name of the room.
|
||||
* See Room Events for more information on m.room.name.
|
||||
*/
|
||||
@Json(name = "name")
|
||||
val name: String?,
|
||||
|
||||
/**
|
||||
* If this is included, an m.room.topic event will be sent into the room to indicate the topic for the room.
|
||||
* See Room Events for more information on m.room.topic.
|
||||
*/
|
||||
@Json(name = "topic")
|
||||
val topic: String?,
|
||||
|
||||
/**
|
||||
* A list of user IDs to invite to the room.
|
||||
* This will tell the server to invite everyone in the list to the newly created room.
|
||||
*/
|
||||
@Json(name = "invite")
|
||||
val invitedUserIds: List<String>?,
|
||||
|
||||
/**
|
||||
* A list of objects representing third party IDs to invite into the room.
|
||||
*/
|
||||
@Json(name = "invite_3pid")
|
||||
val invite3pids: List<ThreePidInviteBody>?,
|
||||
|
||||
/**
|
||||
* Extra keys to be added to the content of the m.room.create.
|
||||
* The server will clobber the following keys: creator.
|
||||
* Future versions of the specification may allow the server to clobber other keys.
|
||||
*/
|
||||
@Json(name = "creation_content")
|
||||
val creationContent: Any?,
|
||||
|
||||
/**
|
||||
* A list of state events to set in the new room.
|
||||
* This allows the user to override the default state events set in the new room.
|
||||
* The expected format of the state events are an object with type, state_key and content keys set.
|
||||
* Takes precedence over events set by presets, but gets overridden by name and topic keys.
|
||||
*/
|
||||
@Json(name = "initial_state")
|
||||
val initialStates: List<Event>?,
|
||||
|
||||
/**
|
||||
* Convenience parameter for setting various default state events based on a preset. Must be either:
|
||||
* private_chat => join_rules is set to invite. history_visibility is set to shared.
|
||||
* trusted_private_chat => join_rules is set to invite. history_visibility is set to shared. All invitees are given the same power level as the
|
||||
* room creator.
|
||||
* public_chat: => join_rules is set to public. history_visibility is set to shared.
|
||||
*/
|
||||
@Json(name = "preset")
|
||||
val preset: CreateRoomPreset?,
|
||||
|
||||
/**
|
||||
* This flag makes the server set the is_direct flag on the m.room.member events sent to the users in invite and invite_3pid.
|
||||
* See Direct Messaging for more information.
|
||||
*/
|
||||
@Json(name = "is_direct")
|
||||
val isDirect: Boolean?,
|
||||
|
||||
/**
|
||||
* The power level content to override in the default power level event
|
||||
*/
|
||||
@Json(name = "power_level_content_override")
|
||||
val powerLevelContentOverride: PowerLevelsContent?
|
||||
)
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* 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.matrix.android.internal.session.room.create
|
||||
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
import im.vector.matrix.android.api.session.identity.IdentityServiceError
|
||||
import im.vector.matrix.android.api.session.identity.toMedium
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import im.vector.matrix.android.internal.di.AuthenticatedIdentity
|
||||
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
|
||||
import im.vector.matrix.android.internal.session.identity.EnsureIdentityTokenTask
|
||||
import im.vector.matrix.android.internal.session.identity.data.IdentityStore
|
||||
import im.vector.matrix.android.internal.session.identity.data.getIdentityServerUrlWithoutProtocol
|
||||
import im.vector.matrix.android.internal.session.room.membership.threepid.ThreePidInviteBody
|
||||
import java.security.InvalidParameterException
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class CreateRoomBodyBuilder @Inject constructor(
|
||||
private val ensureIdentityTokenTask: EnsureIdentityTokenTask,
|
||||
private val crossSigningService: CrossSigningService,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val identityStore: IdentityStore,
|
||||
@AuthenticatedIdentity
|
||||
private val accessTokenProvider: AccessTokenProvider
|
||||
) {
|
||||
|
||||
suspend fun build(params: CreateRoomParams): CreateRoomBody {
|
||||
val invite3pids = params.invite3pids
|
||||
.takeIf { it.isNotEmpty() }
|
||||
.let {
|
||||
// This can throw Exception if Identity server is not configured
|
||||
ensureIdentityTokenTask.execute(Unit)
|
||||
|
||||
val identityServerUrlWithoutProtocol = identityStore.getIdentityServerUrlWithoutProtocol()
|
||||
?: throw IdentityServiceError.NoIdentityServerConfigured
|
||||
val identityServerAccessToken = accessTokenProvider.getToken() ?: throw IdentityServiceError.NoIdentityServerConfigured
|
||||
|
||||
params.invite3pids.map {
|
||||
ThreePidInviteBody(
|
||||
id_server = identityServerUrlWithoutProtocol,
|
||||
id_access_token = identityServerAccessToken,
|
||||
medium = it.toMedium(),
|
||||
address = it.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val initialStates = listOfNotNull(
|
||||
buildEncryptionWithAlgorithmEvent(params),
|
||||
buildHistoryVisibilityEvent(params)
|
||||
)
|
||||
.takeIf { it.isNotEmpty() }
|
||||
|
||||
return CreateRoomBody(
|
||||
visibility = params.visibility,
|
||||
roomAliasName = params.roomAliasName,
|
||||
name = params.name,
|
||||
topic = params.topic,
|
||||
invitedUserIds = params.invitedUserIds,
|
||||
invite3pids = invite3pids,
|
||||
creationContent = params.creationContent,
|
||||
initialStates = initialStates,
|
||||
preset = params.preset,
|
||||
isDirect = params.isDirect,
|
||||
powerLevelContentOverride = params.powerLevelContentOverride
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildHistoryVisibilityEvent(params: CreateRoomParams): Event? {
|
||||
return params.historyVisibility
|
||||
?.let {
|
||||
val contentMap = mapOf("history_visibility" to it)
|
||||
|
||||
Event(
|
||||
type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||
stateKey = "",
|
||||
content = contentMap.toContent())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the crypto algorithm to the room creation parameters.
|
||||
*/
|
||||
private suspend fun buildEncryptionWithAlgorithmEvent(params: CreateRoomParams): Event? {
|
||||
if (params.algorithm == null
|
||||
&& canEnableEncryption(params)) {
|
||||
// Enable the encryption
|
||||
params.enableEncryption()
|
||||
}
|
||||
return params.algorithm
|
||||
?.let {
|
||||
if (it != MXCRYPTO_ALGORITHM_MEGOLM) {
|
||||
throw InvalidParameterException("Unsupported algorithm: $it")
|
||||
}
|
||||
val contentMap = mapOf("algorithm" to it)
|
||||
|
||||
Event(
|
||||
type = EventType.STATE_ROOM_ENCRYPTION,
|
||||
stateKey = "",
|
||||
content = contentMap.toContent()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun canEnableEncryption(params: CreateRoomParams): Boolean {
|
||||
return (params.enableEncryptionIfInvitedUsersSupportIt
|
||||
&& crossSigningService.isCrossSigningVerified()
|
||||
&& params.invite3pids.isEmpty())
|
||||
&& params.invitedUserIds.isNotEmpty()
|
||||
&& params.invitedUserIds.let { userIds ->
|
||||
val keys = deviceListManager.downloadKeys(userIds, forceDownload = false)
|
||||
|
||||
userIds.all { userId ->
|
||||
keys.map[userId].let { deviceMap ->
|
||||
if (deviceMap.isNullOrEmpty()) {
|
||||
// A user has no device, so do not enable encryption
|
||||
false
|
||||
} else {
|
||||
// Check that every user's device have at least one key
|
||||
deviceMap.values.all { !it.keys.isNullOrEmpty() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue