mirror of
https://github.com/vector-im/element-android.git
synced 2024-12-02 16:06:40 +08:00
Merge branch 'release/0.91.5'
This commit is contained in:
commit
b9f0c176d9
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 ✨:
|
Features ✨:
|
||||||
@ -16,7 +54,7 @@ Bugfix 🐛:
|
|||||||
|
|
||||||
Build 🧱:
|
Build 🧱:
|
||||||
- Fix lint false-positive about WorkManager (#1012)
|
- 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
|
- Upgrade gradle from 5.4.1 to 5.6.4
|
||||||
|
|
||||||
Changes in Riot.imX 0.91.3 (2020-07-01)
|
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.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.3.50'
|
ext.kotlin_version = '1.3.72'
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
@ -10,12 +10,13 @@ buildscript {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
dependencies {
|
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.google.gms:google-services:4.3.2'
|
||||||
classpath "com.airbnb.okreplay:gradle-plugin:1.5.0"
|
classpath "com.airbnb.okreplay:gradle-plugin:1.5.0"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1'
|
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
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
@ -38,6 +39,8 @@ allprojects {
|
|||||||
includeGroupByRegex "com\\.github\\.yalantis"
|
includeGroupByRegex "com\\.github\\.yalantis"
|
||||||
// JsonViewer
|
// JsonViewer
|
||||||
includeGroupByRegex 'com\\.github\\.BillCarsonFr'
|
includeGroupByRegex 'com\\.github\\.BillCarsonFr'
|
||||||
|
// PhotoView
|
||||||
|
includeGroupByRegex 'com\\.github\\.chrisbanes'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
maven {
|
maven {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
Useful links:
|
Useful links:
|
||||||
- https://codelabs.developers.google.com/codelabs/webrtc-web/#0
|
- https://codelabs.developers.google.com/codelabs/webrtc-web/#0
|
||||||
|
- http://webrtc.github.io/webrtc-org/native-code/android/
|
||||||
|
|
||||||
|
|
||||||
╔════════════════════════════════════════════════╗
|
╔════════════════════════════════════════════════╗
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
# The setting is particularly useful for tweaking memory settings.
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
org.gradle.jvmargs=-Xmx8192m
|
org.gradle.jvmargs=-Xmx2048m
|
||||||
# When configured, Gradle will run in incubating parallel mode.
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
# This option should only be used with decoupled projects. More details, visit
|
# 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
|
# 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:rxkotlin:2.3.0'
|
||||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||||
// Paging
|
// Paging
|
||||||
implementation "androidx.paging:paging-runtime-ktx:2.1.0"
|
implementation "androidx.paging:paging-runtime-ktx:2.1.2"
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||||
|
@ -19,6 +19,7 @@ package im.vector.matrix.rx
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import im.vector.matrix.android.api.query.QueryStringValue
|
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.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.Room
|
||||||
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
|
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
|
||||||
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
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>> {
|
fun liveReadMarker(): Observable<Optional<String>> {
|
||||||
return room.getReadMarkerLive().asObservable()
|
return room.getReadMarkerLive().asObservable()
|
||||||
}
|
}
|
||||||
@ -104,6 +112,10 @@ class RxRoom(private val room: Room) {
|
|||||||
room.invite(userId, reason, it)
|
room.invite(userId, reason, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun invite3pid(threePid: ThreePid): Completable = completableBuilder<Unit> {
|
||||||
|
room.invite3pid(threePid, it)
|
||||||
|
}
|
||||||
|
|
||||||
fun updateTopic(topic: String): Completable = completableBuilder<Unit> {
|
fun updateTopic(topic: String): Completable = completableBuilder<Unit> {
|
||||||
room.updateTopic(topic, it)
|
room.updateTopic(topic, it)
|
||||||
}
|
}
|
||||||
|
@ -17,14 +17,20 @@
|
|||||||
package im.vector.matrix.rx
|
package im.vector.matrix.rx
|
||||||
|
|
||||||
import androidx.paging.PagedList
|
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.query.QueryStringValue
|
||||||
import im.vector.matrix.android.api.session.Session
|
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.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.GroupSummaryQueryParams
|
||||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||||
import im.vector.matrix.android.api.session.identity.ThreePid
|
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.pushers.Pusher
|
||||||
import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
|
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.RoomSummary
|
||||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||||
import im.vector.matrix.android.api.session.sync.SyncState
|
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.CryptoDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
|
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 im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
|
import io.reactivex.functions.Function3
|
||||||
|
|
||||||
class RxSession(private val session: Session) {
|
class RxSession(private val session: Session) {
|
||||||
|
|
||||||
@ -165,6 +173,42 @@ class RxSession(private val session: Session) {
|
|||||||
session.widgetService().getRoomWidgets(roomId, widgetId, widgetTypes, excludedTypes)
|
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 {
|
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 {
|
buildTypes {
|
||||||
|
|
||||||
debug {
|
debug {
|
||||||
// Set to true to log privacy or sensible data, such as token
|
// Set to true to log privacy or sensible data, such as token
|
||||||
buildConfigField "boolean", "LOG_PRIVATE_DATA", project.property("vector.debugPrivateData")
|
buildConfigField "boolean", "LOG_PRIVATE_DATA", project.property("vector.debugPrivateData")
|
||||||
@ -123,7 +122,7 @@ dependencies {
|
|||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||||
|
|
||||||
implementation "androidx.appcompat:appcompat:1.1.0"
|
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-extensions:$lifecycle_version"
|
||||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
||||||
@ -205,5 +204,4 @@ dependencies {
|
|||||||
androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
||||||
|
|
||||||
androidTestUtil 'androidx.test:orchestrator:1.2.0'
|
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 aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
|
||||||
|
|
||||||
val roomId = mTestHelper.doSync<String> {
|
val roomId = mTestHelper.doSync<String> {
|
||||||
aliceSession.createRoom(CreateRoomParams(name = "MyRoom"), it)
|
aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" }, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (encryptedRoom) {
|
if (encryptedRoom) {
|
||||||
@ -175,7 +175,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mTestHelper.doSync<Unit> {
|
mTestHelper.doSync<Unit> {
|
||||||
samSession.joinRoom(room.roomId, null, it)
|
samSession.joinRoom(room.roomId, null, emptyList(), it)
|
||||||
}
|
}
|
||||||
|
|
||||||
return samSession
|
return samSession
|
||||||
@ -286,9 +286,11 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
|||||||
fun createDM(alice: Session, bob: Session): String {
|
fun createDM(alice: Session, bob: Session): String {
|
||||||
val roomId = mTestHelper.doSync<String> {
|
val roomId = mTestHelper.doSync<String> {
|
||||||
alice.createRoom(
|
alice.createRoom(
|
||||||
CreateRoomParams(invitedUserIds = listOf(bob.myUserId))
|
CreateRoomParams().apply {
|
||||||
.setDirectMessage()
|
invitedUserIds.add(bob.myUserId)
|
||||||
.enableEncryptionIfInvitedUsersSupportIt(),
|
setDirectMessage()
|
||||||
|
enableEncryptionIfInvitedUsersSupportIt = true
|
||||||
|
},
|
||||||
it
|
it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,10 @@ class KeyShareTests : InstrumentedTest {
|
|||||||
// Create an encrypted room and add a message
|
// Create an encrypted room and add a message
|
||||||
val roomId = mTestHelper.doSync<String> {
|
val roomId = mTestHelper.doSync<String> {
|
||||||
aliceSession.createRoom(
|
aliceSession.createRoom(
|
||||||
CreateRoomParams(RoomDirectoryVisibility.PRIVATE).enableEncryptionWithAlgorithm(true),
|
CreateRoomParams().apply {
|
||||||
|
visibility = RoomDirectoryVisibility.PRIVATE
|
||||||
|
enableEncryption()
|
||||||
|
},
|
||||||
it
|
it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -285,7 +288,7 @@ class KeyShareTests : InstrumentedTest {
|
|||||||
mTestHelper.waitWithLatch(60_000) { latch ->
|
mTestHelper.waitWithLatch(60_000) { latch ->
|
||||||
val keysBackupService = aliceSession2.cryptoService().keysBackupService()
|
val keysBackupService = aliceSession2.cryptoService().keysBackupService()
|
||||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
Log.d("#TEST", "Recovery :${ keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
|
Log.d("#TEST", "Recovery :${keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
|
||||||
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey
|
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ data class MatrixConfiguration(
|
|||||||
),
|
),
|
||||||
/**
|
/**
|
||||||
* Optional proxy to connect to the matrix servers
|
* Optional proxy to connect to the matrix servers
|
||||||
* You can create one using for instance Proxy(proxyType, InetSocketAddress(hostname, port)
|
* You can create one using for instance Proxy(proxyType, InetSocketAddress.createUnresolved(hostname, port)
|
||||||
*/
|
*/
|
||||||
val proxy: Proxy? = null
|
val proxy: Proxy? = null
|
||||||
) {
|
) {
|
||||||
|
@ -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.typing.TypingUsersTracker
|
||||||
import im.vector.matrix.android.api.session.user.UserService
|
import im.vector.matrix.android.api.session.user.UserService
|
||||||
import im.vector.matrix.android.api.session.widgets.WidgetService
|
import im.vector.matrix.android.api.session.widgets.WidgetService
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This interface defines interactions with a session.
|
* This interface defines interactions with a session.
|
||||||
@ -205,6 +206,13 @@ interface Session :
|
|||||||
*/
|
*/
|
||||||
fun removeListener(listener: Listener)
|
fun removeListener(listener: Listener)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will return a OkHttpClient which will manage pinned certificates and Proxy if configured.
|
||||||
|
* It will not add any access-token to the request.
|
||||||
|
* So it is exposed to let the app be able to download image with Glide or any other libraries which accept an OkHttp client.
|
||||||
|
*/
|
||||||
|
fun getOkHttpClient(): OkHttpClient
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A global session listener to get notified for some events.
|
* A global session listener to get notified for some events.
|
||||||
*/
|
*/
|
||||||
|
@ -61,6 +61,8 @@ interface CrossSigningService {
|
|||||||
|
|
||||||
fun canCrossSign(): Boolean
|
fun canCrossSign(): Boolean
|
||||||
|
|
||||||
|
fun allPrivateKeysKnown(): Boolean
|
||||||
|
|
||||||
fun trustUser(otherUserId: String,
|
fun trustUser(otherUserId: String,
|
||||||
callback: MatrixCallback<Unit>)
|
callback: MatrixCallback<Unit>)
|
||||||
|
|
||||||
|
@ -39,5 +39,10 @@ data class UnsignedData(
|
|||||||
* Optional. The previous content for this event. If there is no previous content, this key will be missing.
|
* 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 = "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
|
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.
|
* This interface defines methods to interact within a group.
|
||||||
*/
|
*/
|
||||||
interface Group {
|
interface Group {
|
||||||
val groupId: String
|
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,
|
publicRoomsParams: PublicRoomsParams,
|
||||||
callback: MatrixCallback<PublicRoomsResponse>): Cancelable
|
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.
|
* Fetches the overall metadata about protocols supported by the homeserver.
|
||||||
* Includes both the available protocols and all fields required for queries against each protocol.
|
* 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 androidx.lifecycle.LiveData
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
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.RoomSummary
|
||||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
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.Cancelable
|
||||||
@ -104,5 +105,13 @@ interface RoomService {
|
|||||||
searchOnServer: Boolean,
|
searchOnServer: Boolean,
|
||||||
callback: MatrixCallback<Optional<String>>): Cancelable
|
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]
|
* [im.vector.matrix.android.api.session.room.Room] and [im.vector.matrix.android.api.session.room.RoomService]
|
||||||
*/
|
*/
|
||||||
data class RoomSummaryQueryParams(
|
data class RoomSummaryQueryParams(
|
||||||
|
val roomId: QueryStringValue,
|
||||||
val displayName: QueryStringValue,
|
val displayName: QueryStringValue,
|
||||||
val canonicalAlias: QueryStringValue,
|
val canonicalAlias: QueryStringValue,
|
||||||
val memberships: List<Membership>
|
val memberships: List<Membership>
|
||||||
@ -35,11 +36,13 @@ data class RoomSummaryQueryParams(
|
|||||||
|
|
||||||
class Builder {
|
class Builder {
|
||||||
|
|
||||||
|
var roomId: QueryStringValue = QueryStringValue.IsNotEmpty
|
||||||
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
|
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
|
||||||
var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition
|
var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition
|
||||||
var memberships: List<Membership> = Membership.all()
|
var memberships: List<Membership> = Membership.all()
|
||||||
|
|
||||||
fun build() = RoomSummaryQueryParams(
|
fun build() = RoomSummaryQueryParams(
|
||||||
|
roomId = roomId,
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
canonicalAlias = canonicalAlias,
|
canonicalAlias = canonicalAlias,
|
||||||
memberships = memberships
|
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 androidx.lifecycle.LiveData
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
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.session.room.model.RoomMemberSummary
|
||||||
import im.vector.matrix.android.api.util.Cancelable
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
|
|
||||||
@ -63,6 +64,12 @@ interface MembershipService {
|
|||||||
reason: String? = null,
|
reason: String? = null,
|
||||||
callback: MatrixCallback<Unit>): Cancelable
|
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
|
* 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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -16,253 +16,102 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.api.session.room.model.create
|
package im.vector.matrix.android.api.session.room.model.create
|
||||||
|
|
||||||
import android.util.Patterns
|
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||||
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.room.model.PowerLevelsContent
|
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.RoomDirectoryVisibility
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
|
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 im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
/**
|
// TODO Give a way to include other initial states
|
||||||
* Parameter to create a room, with facilities functions to configure it
|
class CreateRoomParams {
|
||||||
*/
|
/**
|
||||||
@JsonClass(generateAdapter = true)
|
* A public visibility indicates that the room will be shown in the published room list.
|
||||||
data class CreateRoomParams(
|
* A private visibility will hide the room from the published room list.
|
||||||
/**
|
* Rooms default to private visibility if this key is not included.
|
||||||
* A public visibility indicates that the room will be shown in the published room list.
|
* NB: This should not be confused with join_rules which also uses the word public. One of: ["public", "private"]
|
||||||
* A private visibility will hide the room from the published room list.
|
*/
|
||||||
* Rooms default to private visibility if this key is not included.
|
var visibility: RoomDirectoryVisibility? = null
|
||||||
* 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
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
* 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 {
|
var enableEncryptionIfInvitedUsersSupportIt: Boolean = false
|
||||||
enableEncryptionIfInvitedUsersSupportIt = value
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the crypto algorithm to the room creation parameters.
|
* 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.
|
||||||
* @param enable true to enable encryption.
|
* 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
|
||||||
* @param algorithm the algorithm, default to [MXCRYPTO_ALGORITHM_MEGOLM], which is actually the only supported algorithm for the moment
|
* room creator.
|
||||||
* @return a modified copy of the CreateRoomParams object, or this if there is no modification
|
* public_chat: => join_rules is set to public. history_visibility is set to shared.
|
||||||
*/
|
*/
|
||||||
@CheckResult
|
var preset: CreateRoomPreset? = null
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force the history visibility in the room creation parameters.
|
* 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.
|
||||||
* @param historyVisibility the expected history visibility, set null to remove any existing value.
|
|
||||||
* @return a modified copy of the CreateRoomParams object
|
|
||||||
*/
|
*/
|
||||||
@CheckResult
|
var isDirect: Boolean? = null
|
||||||
fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?): CreateRoomParams {
|
|
||||||
// Remove the existing value if any.
|
|
||||||
val newInitialStates = initialStates
|
|
||||||
?.filter { it.type != EventType.STATE_ROOM_HISTORY_VISIBILITY }
|
|
||||||
|
|
||||||
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,
|
* The power level content to override in the default power level event
|
||||||
stateKey = "",
|
*/
|
||||||
content = contentMap.toContent())
|
var powerLevelContentOverride: PowerLevelsContent? = null
|
||||||
|
|
||||||
return copy(
|
|
||||||
initialStates = newInitialStates.orEmpty() + historyVisibilityEvent
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return copy(
|
|
||||||
initialStates = newInitialStates
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark as a direct message room.
|
* Mark as a direct message room.
|
||||||
* @return a modified copy of the CreateRoomParams object
|
|
||||||
*/
|
*/
|
||||||
@CheckResult
|
fun setDirectMessage() {
|
||||||
fun setDirectMessage(): CreateRoomParams {
|
preset = CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
|
||||||
return copy(
|
isDirect = true
|
||||||
preset = CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT,
|
|
||||||
isDirect = true
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tells if the created room can be a direct chat one.
|
* Supported value: MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
*
|
|
||||||
* @return true if it is a direct chat
|
|
||||||
*/
|
*/
|
||||||
fun isDirect(): Boolean {
|
var algorithm: String? = null
|
||||||
return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
|
private set
|
||||||
&& isDirect == true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
var historyVisibility: RoomHistoryVisibility? = null
|
||||||
* @return the first invited user id
|
|
||||||
*/
|
|
||||||
fun getFirstInvitedUserId(): String? {
|
|
||||||
return invitedUserIds?.firstOrNull() ?: invite3pids?.firstOrNull()?.address
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
fun enableEncryption() {
|
||||||
* Add some ids to the room creation
|
algorithm = MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
* 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
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
|
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -124,59 +123,4 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
|
|||||||
else -> Role.Moderator.value
|
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 getTimeLineEvent(eventId: String): TimelineEvent?
|
||||||
|
|
||||||
fun getTimeLineEventLive(eventId: String): LiveData<Optional<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.MatrixCallback
|
||||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
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.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.SELF_SIGNING_KEY_SSSS_NAME
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||||
@ -124,6 +125,13 @@ interface SharedSecretStorageService {
|
|||||||
) is IntegrityResult.Success
|
) 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 checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?): IntegrityResult
|
||||||
|
|
||||||
fun requestSecret(name: String, myOtherDeviceId: String)
|
fun requestSecret(name: String, myOtherDeviceId: String)
|
||||||
|
@ -71,8 +71,8 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
|
|||||||
delay(1500)
|
delay(1500)
|
||||||
cryptoStore.getOrAddOutgoingSecretShareRequest(secretName, recipients)?.let {
|
cryptoStore.getOrAddOutgoingSecretShareRequest(secretName, recipients)?.let {
|
||||||
// TODO check if there is already one that is being sent?
|
// TODO check if there is already one that is being sent?
|
||||||
if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
|
if (it.state == OutgoingGossipingRequestState.SENDING /**|| it.state == OutgoingGossipingRequestState.SENT*/) {
|
||||||
Timber.v("## CRYPTO - GOSSIP sendSecretShareRequest() : we already request for that session: $it")
|
Timber.v("## CRYPTO - GOSSIP sendSecretShareRequest() : we are already sending for that session: $it")
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.crosssigning
|
|||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
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.CrossSigningService
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||||
import im.vector.matrix.android.api.util.Optional
|
import im.vector.matrix.android.api.util.Optional
|
||||||
@ -507,6 +508,11 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
&& cryptoStore.getCrossSigningPrivateKeys()?.user != null
|
&& cryptoStore.getCrossSigningPrivateKeys()?.user != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun allPrivateKeysKnown(): Boolean {
|
||||||
|
return checkSelfTrust().isVerified()
|
||||||
|
&& cryptoStore.getCrossSigningPrivateKeys()?.allKnown().orFalse()
|
||||||
|
}
|
||||||
|
|
||||||
override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {
|
override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
Timber.d("## CrossSigning - Mark user $userId as trusted ")
|
Timber.d("## CrossSigning - Mark user $userId as trusted ")
|
||||||
|
@ -20,4 +20,6 @@ data class PrivateKeysInfo(
|
|||||||
val master: String? = null,
|
val master: String? = null,
|
||||||
val selfSigned: String? = null,
|
val selfSigned: String? = null,
|
||||||
val user: 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
|
// We can SCAN or SHOW QR codes only if cross-signing is enabled
|
||||||
val methodValues = if (crossSigningService.isCrossSigningVerified()) {
|
val methodValues = if (crossSigningService.isCrossSigningInitialized()) {
|
||||||
// Add reciprocate method if application declares it can scan or show QR codes
|
// Add reciprocate method if application declares it can scan or show QR codes
|
||||||
// Not sure if it ok to do that (?)
|
// Not sure if it ok to do that (?)
|
||||||
val reciprocateMethod = methods
|
val reciprocateMethod = methods
|
||||||
|
@ -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 com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.internal.session.SessionLifecycleObserver
|
import im.vector.matrix.android.internal.session.SessionLifecycleObserver
|
||||||
import im.vector.matrix.android.internal.util.createBackgroundHandler
|
import im.vector.matrix.android.internal.util.createBackgroundHandler
|
||||||
import io.realm.OrderedRealmCollectionChangeListener
|
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmChangeListener
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
import io.realm.RealmObject
|
import io.realm.RealmObject
|
||||||
import io.realm.RealmResults
|
import io.realm.RealmResults
|
||||||
@ -30,10 +30,10 @@ import kotlinx.coroutines.cancelChildren
|
|||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
internal interface LiveEntityObserver: SessionLifecycleObserver
|
internal interface LiveEntityObserver : SessionLifecycleObserver
|
||||||
|
|
||||||
internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val realmConfiguration: RealmConfiguration)
|
internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val realmConfiguration: RealmConfiguration)
|
||||||
: LiveEntityObserver, OrderedRealmCollectionChangeListener<RealmResults<T>> {
|
: LiveEntityObserver, RealmChangeListener<RealmResults<T>> {
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
val BACKGROUND_HANDLER = createBackgroundHandler("LIVE_ENTITY_BACKGROUND")
|
val BACKGROUND_HANDLER = createBackgroundHandler("LIVE_ENTITY_BACKGROUND")
|
||||||
|
@ -115,6 +115,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
numberOfTimelineEvents++
|
||||||
timelineEvents.add(timelineEventEntity)
|
timelineEvents.add(timelineEventEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,17 +123,18 @@ private fun computeIsUnique(
|
|||||||
realm: Realm,
|
realm: Realm,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
isLastForward: Boolean,
|
isLastForward: Boolean,
|
||||||
myRoomMemberContent: RoomMemberContent,
|
senderRoomMemberContent: RoomMemberContent,
|
||||||
roomMemberContentsByUser: Map<String, RoomMemberContent?>
|
roomMemberContentsByUser: Map<String, RoomMemberContent?>
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val isHistoricalUnique = roomMemberContentsByUser.values.find {
|
val isHistoricalUnique = roomMemberContentsByUser.values.find {
|
||||||
it != myRoomMemberContent && it?.displayName == myRoomMemberContent.displayName
|
it != senderRoomMemberContent && it?.displayName == senderRoomMemberContent.displayName
|
||||||
} == null
|
} == null
|
||||||
return if (isLastForward) {
|
return if (isLastForward) {
|
||||||
val isLiveUnique = RoomMemberSummaryEntity
|
val isLiveUnique = RoomMemberSummaryEntity
|
||||||
.where(realm, roomId)
|
.where(realm, roomId)
|
||||||
.equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, myRoomMemberContent.displayName)
|
.equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, senderRoomMemberContent.displayName)
|
||||||
.findAll().none {
|
.findAll()
|
||||||
|
.none {
|
||||||
!roomMemberContentsByUser.containsKey(it.userId)
|
!roomMemberContentsByUser.containsKey(it.userId)
|
||||||
}
|
}
|
||||||
isHistoricalUnique && isLiveUnique
|
isHistoricalUnique && isLiveUnique
|
||||||
|
@ -45,7 +45,6 @@ internal object EventMapper {
|
|||||||
eventEntity.redacts = event.redacts
|
eventEntity.redacts = event.redacts
|
||||||
eventEntity.age = event.unsignedData?.age ?: event.originServerTs
|
eventEntity.age = event.unsignedData?.age ?: event.originServerTs
|
||||||
eventEntity.unsignedData = uds
|
eventEntity.unsignedData = uds
|
||||||
|
|
||||||
eventEntity.decryptionResultJson = event.mxDecryptionResult?.let {
|
eventEntity.decryptionResultJson = event.mxDecryptionResult?.let {
|
||||||
MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java).toJson(it)
|
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,
|
@Index var nextToken: String? = null,
|
||||||
var stateEvents: RealmList<EventEntity> = RealmList(),
|
var stateEvents: RealmList<EventEntity> = RealmList(),
|
||||||
var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
|
var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
|
||||||
|
var numberOfTimelineEvents: Long = 0,
|
||||||
// Only one chunk will have isLastForward == true
|
// Only one chunk will have isLastForward == true
|
||||||
@Index var isLastForward: Boolean = false,
|
@Index var isLastForward: Boolean = false,
|
||||||
@Index var isLastBackward: 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.
|
* 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
|
* Then GetGroupDataTask is called regularly to fetch group information from the homeserver.
|
||||||
* makes requests to fetch group information from the homeserver
|
|
||||||
*/
|
*/
|
||||||
internal open class GroupEntity(@PrimaryKey var groupId: String = "")
|
internal open class GroupEntity(@PrimaryKey var groupId: String = "")
|
||||||
: RealmObject() {
|
: RealmObject() {
|
||||||
|
@ -24,7 +24,7 @@ import io.realm.annotations.PrimaryKey
|
|||||||
internal open class RoomMemberSummaryEntity(@PrimaryKey var primaryKey: String = "",
|
internal open class RoomMemberSummaryEntity(@PrimaryKey var primaryKey: String = "",
|
||||||
@Index var userId: String = "",
|
@Index var userId: String = "",
|
||||||
@Index var roomId: String = "",
|
@Index var roomId: String = "",
|
||||||
var displayName: String? = null,
|
@Index var displayName: String? = null,
|
||||||
var avatarUrl: String? = null,
|
var avatarUrl: String? = null,
|
||||||
var reason: String? = null,
|
var reason: String? = null,
|
||||||
var isDirect: Boolean = false
|
var isDirect: Boolean = false
|
||||||
|
@ -25,6 +25,7 @@ import io.realm.annotations.RealmModule
|
|||||||
classes = [
|
classes = [
|
||||||
ChunkEntity::class,
|
ChunkEntity::class,
|
||||||
EventEntity::class,
|
EventEntity::class,
|
||||||
|
EventInsertEntity::class,
|
||||||
TimelineEventEntity::class,
|
TimelineEventEntity::class,
|
||||||
FilterEntity::class,
|
FilterEntity::class,
|
||||||
GroupEntity::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.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
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.Realm
|
||||||
import io.realm.RealmList
|
import io.realm.RealmList
|
||||||
import io.realm.RealmQuery
|
import io.realm.RealmQuery
|
||||||
import io.realm.kotlin.where
|
import io.realm.kotlin.where
|
||||||
|
|
||||||
internal fun EventEntity.copyToRealmOrIgnore(realm: Realm): EventEntity {
|
internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInsertType): EventEntity {
|
||||||
return realm.where<EventEntity>()
|
val eventEntity = realm.where<EventEntity>()
|
||||||
.equalTo(EventEntityFields.EVENT_ID, eventId)
|
.equalTo(EventEntityFields.EVENT_ID, eventId)
|
||||||
.equalTo(EventEntityFields.ROOM_ID, roomId)
|
.equalTo(EventEntityFields.ROOM_ID, roomId)
|
||||||
.findFirst() ?: realm.copyToRealm(this)
|
.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> {
|
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.api.session.room.model.Membership
|
||||||
import im.vector.matrix.android.internal.database.model.GroupEntity
|
import im.vector.matrix.android.internal.database.model.GroupEntity
|
||||||
import im.vector.matrix.android.internal.database.model.GroupEntityFields
|
import im.vector.matrix.android.internal.database.model.GroupEntityFields
|
||||||
|
import im.vector.matrix.android.internal.query.process
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmQuery
|
import io.realm.RealmQuery
|
||||||
import io.realm.kotlin.where
|
import io.realm.kotlin.where
|
||||||
@ -28,10 +29,6 @@ internal fun GroupEntity.Companion.where(realm: Realm, groupId: String): RealmQu
|
|||||||
.equalTo(GroupEntityFields.GROUP_ID, groupId)
|
.equalTo(GroupEntityFields.GROUP_ID, groupId)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun GroupEntity.Companion.where(realm: Realm, membership: Membership? = null): RealmQuery<GroupEntity> {
|
internal fun GroupEntity.Companion.where(realm: Realm, memberships: List<Membership>): RealmQuery<GroupEntity> {
|
||||||
val query = realm.where<GroupEntity>()
|
return realm.where<GroupEntity>().process(GroupEntityFields.MEMBERSHIP_STR, memberships)
|
||||||
if (membership != null) {
|
|
||||||
query.equalTo(GroupEntityFields.MEMBERSHIP_STR, membership.name)
|
|
||||||
}
|
|
||||||
return query
|
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ 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.model.GroupSummaryEntityFields
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmQuery
|
import io.realm.RealmQuery
|
||||||
|
import io.realm.kotlin.createObject
|
||||||
import io.realm.kotlin.where
|
import io.realm.kotlin.where
|
||||||
|
|
||||||
internal fun GroupSummaryEntity.Companion.where(realm: Realm, groupId: String? = null): RealmQuery<GroupSummaryEntity> {
|
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>()
|
return realm.where<GroupSummaryEntity>()
|
||||||
.`in`(GroupSummaryEntityFields.GROUP_ID, groupIds.toTypedArray())
|
.`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.ListenableWorker
|
||||||
import androidx.work.NetworkType
|
import androidx.work.NetworkType
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class WorkManagerProvider @Inject constructor(
|
internal class WorkManagerProvider @Inject constructor(
|
||||||
@ -39,6 +41,14 @@ internal class WorkManagerProvider @Inject constructor(
|
|||||||
OneTimeWorkRequestBuilder<W>()
|
OneTimeWorkRequestBuilder<W>()
|
||||||
.addTag(tag)
|
.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
|
* 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.api.session.widgets.WidgetService
|
||||||
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
||||||
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
|
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
|
||||||
|
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||||
import im.vector.matrix.android.internal.di.SessionId
|
import im.vector.matrix.android.internal.di.SessionId
|
||||||
|
import im.vector.matrix.android.internal.di.UnauthenticatedWithCertificate
|
||||||
import im.vector.matrix.android.internal.di.WorkManagerProvider
|
import im.vector.matrix.android.internal.di.WorkManagerProvider
|
||||||
import im.vector.matrix.android.internal.session.identity.DefaultIdentityService
|
import im.vector.matrix.android.internal.session.identity.DefaultIdentityService
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecryptor
|
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecryptor
|
||||||
@ -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.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
import im.vector.matrix.android.internal.util.createUIHandler
|
import im.vector.matrix.android.internal.util.createUIHandler
|
||||||
|
import io.realm.RealmConfiguration
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.greenrobot.eventbus.Subscribe
|
import org.greenrobot.eventbus.Subscribe
|
||||||
import org.greenrobot.eventbus.ThreadMode
|
import org.greenrobot.eventbus.ThreadMode
|
||||||
@ -76,6 +80,7 @@ internal class DefaultSession @Inject constructor(
|
|||||||
private val eventBus: EventBus,
|
private val eventBus: EventBus,
|
||||||
@SessionId
|
@SessionId
|
||||||
override val sessionId: String,
|
override val sessionId: String,
|
||||||
|
@SessionDatabase private val realmConfiguration: RealmConfiguration,
|
||||||
private val lifecycleObservers: Set<@JvmSuppressWildcards SessionLifecycleObserver>,
|
private val lifecycleObservers: Set<@JvmSuppressWildcards SessionLifecycleObserver>,
|
||||||
private val sessionListeners: SessionListeners,
|
private val sessionListeners: SessionListeners,
|
||||||
private val roomService: Lazy<RoomService>,
|
private val roomService: Lazy<RoomService>,
|
||||||
@ -110,8 +115,10 @@ internal class DefaultSession @Inject constructor(
|
|||||||
private val defaultIdentityService: DefaultIdentityService,
|
private val defaultIdentityService: DefaultIdentityService,
|
||||||
private val integrationManagerService: IntegrationManagerService,
|
private val integrationManagerService: IntegrationManagerService,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val callSignalingService: Lazy<CallSignalingService>)
|
private val callSignalingService: Lazy<CallSignalingService>,
|
||||||
: Session,
|
@UnauthenticatedWithCertificate
|
||||||
|
private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>
|
||||||
|
) : Session,
|
||||||
RoomService by roomService.get(),
|
RoomService by roomService.get(),
|
||||||
RoomDirectoryService by roomDirectoryService.get(),
|
RoomDirectoryService by roomDirectoryService.get(),
|
||||||
GroupService by groupService.get(),
|
GroupService by groupService.get(),
|
||||||
@ -252,6 +259,10 @@ internal class DefaultSession @Inject constructor(
|
|||||||
|
|
||||||
override fun callSignalingService(): CallSignalingService = callSignalingService.get()
|
override fun callSignalingService(): CallSignalingService = callSignalingService.get()
|
||||||
|
|
||||||
|
override fun getOkHttpClient(): OkHttpClient {
|
||||||
|
return unauthenticatedWithCertificateOkHttpClient.get()
|
||||||
|
}
|
||||||
|
|
||||||
override fun addListener(listener: Session.Listener) {
|
override fun addListener(listener: Session.Listener) {
|
||||||
sessionListeners.addListener(listener)
|
sessionListeners.addListener(listener)
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
internal class SessionListeners @Inject constructor() {
|
||||||
|
|
||||||
private val listeners = ArrayList<Session.Listener>()
|
private val listeners = mutableSetOf<Session.Listener>()
|
||||||
|
|
||||||
fun addListener(listener: Session.Listener) {
|
fun addListener(listener: Session.Listener) {
|
||||||
synchronized(listeners) {
|
synchronized(listeners) {
|
||||||
|
@ -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.api.session.typing.TypingUsersTracker
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.ShieldTrustUpdater
|
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.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.database.SessionRealmConfigurationFactory
|
||||||
import im.vector.matrix.android.internal.di.Authenticated
|
import im.vector.matrix.android.internal.di.Authenticated
|
||||||
import im.vector.matrix.android.internal.di.DeviceId
|
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.interceptors.CurlLoggingInterceptor
|
||||||
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
|
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
|
||||||
import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider
|
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.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.homeserver.DefaultHomeServerCapabilitiesService
|
||||||
import im.vector.matrix.android.internal.session.identity.DefaultIdentityService
|
import im.vector.matrix.android.internal.session.identity.DefaultIdentityService
|
||||||
import im.vector.matrix.android.internal.session.integrationmanager.IntegrationManager
|
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.EventRelationsAggregationProcessor
|
||||||
import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLiveObserver
|
import im.vector.matrix.android.internal.session.room.create.RoomCreateEventProcessor
|
||||||
import im.vector.matrix.android.internal.session.room.prune.EventsPruner
|
import im.vector.matrix.android.internal.session.room.prune.RedactionEventProcessor
|
||||||
import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventLiveObserver
|
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.securestorage.DefaultSecureStorageService
|
||||||
import im.vector.matrix.android.internal.session.typing.DefaultTypingUsersTracker
|
import im.vector.matrix.android.internal.session.typing.DefaultTypingUsersTracker
|
||||||
import im.vector.matrix.android.internal.session.user.accountdata.DefaultAccountDataService
|
import im.vector.matrix.android.internal.session.user.accountdata.DefaultAccountDataService
|
||||||
@ -293,31 +294,31 @@ internal abstract class SessionModule {
|
|||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoSet
|
@IntoSet
|
||||||
abstract fun bindGroupSummaryUpdater(updater: GroupSummaryUpdater): SessionLifecycleObserver
|
abstract fun bindEventRedactionProcessor(processor: RedactionEventProcessor): EventInsertLiveProcessor
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoSet
|
@IntoSet
|
||||||
abstract fun bindEventsPruner(pruner: EventsPruner): SessionLifecycleObserver
|
abstract fun bindEventRelationsAggregationProcessor(processor: EventRelationsAggregationProcessor): EventInsertLiveProcessor
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoSet
|
@IntoSet
|
||||||
abstract fun bindEventRelationsAggregationUpdater(updater: EventRelationsAggregationUpdater): SessionLifecycleObserver
|
abstract fun bindRoomTombstoneEventProcessor(processor: RoomTombstoneEventProcessor): EventInsertLiveProcessor
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoSet
|
@IntoSet
|
||||||
abstract fun bindRoomTombstoneEventLiveObserver(observer: RoomTombstoneEventLiveObserver): SessionLifecycleObserver
|
abstract fun bindRoomCreateEventProcessor(processor: RoomCreateEventProcessor): EventInsertLiveProcessor
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoSet
|
@IntoSet
|
||||||
abstract fun bindRoomCreateEventLiveObserver(observer: RoomCreateEventLiveObserver): SessionLifecycleObserver
|
abstract fun bindVerificationMessageProcessor(processor: VerificationMessageProcessor): EventInsertLiveProcessor
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoSet
|
@IntoSet
|
||||||
abstract fun bindVerificationMessageLiveObserver(observer: VerificationMessageLiveObserver): SessionLifecycleObserver
|
abstract fun bindCallEventProcessor(processor: CallEventProcessor): EventInsertLiveProcessor
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoSet
|
@IntoSet
|
||||||
abstract fun bindCallEventObserver(observer: CallEventObserver): SessionLifecycleObserver
|
abstract fun bindEventInsertObserver(observer: EventInsertLiveObserver): SessionLifecycleObserver
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoSet
|
@IntoSet
|
||||||
@ -335,6 +336,10 @@ internal abstract class SessionModule {
|
|||||||
@IntoSet
|
@IntoSet
|
||||||
abstract fun bindIdentityService(observer: DefaultIdentityService): SessionLifecycleObserver
|
abstract fun bindIdentityService(observer: DefaultIdentityService): SessionLifecycleObserver
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoSet
|
||||||
|
abstract fun bindDatabaseCleaner(observer: DatabaseCleaner): SessionLifecycleObserver
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindInitialSyncProgressService(service: DefaultInitialSyncProgressService): InitialSyncProgressService
|
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
|
@Binds
|
||||||
abstract fun bindGetTurnServerTask(task: DefaultGetTurnServerTask): GetTurnServerTask
|
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 com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.session.room.model.Membership
|
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.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.database.query.where
|
||||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||||
import im.vector.matrix.android.internal.network.executeRequest
|
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.task.Task
|
||||||
import im.vector.matrix.android.internal.util.awaitTransaction
|
import im.vector.matrix.android.internal.util.awaitTransaction
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal interface GetGroupDataTask : Task<GetGroupDataTask.Params, Unit> {
|
internal interface GetGroupDataTask : Task<GetGroupDataTask.Params, Unit> {
|
||||||
|
sealed class Params {
|
||||||
data class Params(val groupId: String)
|
object FetchAllActive : Params()
|
||||||
|
data class FetchWithIds(val groupIds: List<String>) : Params()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class DefaultGetGroupDataTask @Inject constructor(
|
internal class DefaultGetGroupDataTask @Inject constructor(
|
||||||
@ -41,44 +46,64 @@ internal class DefaultGetGroupDataTask @Inject constructor(
|
|||||||
private val eventBus: EventBus
|
private val eventBus: EventBus
|
||||||
) : GetGroupDataTask {
|
) : GetGroupDataTask {
|
||||||
|
|
||||||
|
private data class GroupData(
|
||||||
|
val groupId: String,
|
||||||
|
val groupSummary: GroupSummaryResponse,
|
||||||
|
val groupRooms: GroupRooms,
|
||||||
|
val groupUsers: GroupUsers
|
||||||
|
)
|
||||||
|
|
||||||
override suspend fun execute(params: GetGroupDataTask.Params) {
|
override suspend fun execute(params: GetGroupDataTask.Params) {
|
||||||
val groupId = params.groupId
|
val groupIds = when (params) {
|
||||||
val groupSummary = executeRequest<GroupSummaryResponse>(eventBus) {
|
is GetGroupDataTask.Params.FetchAllActive -> {
|
||||||
apiCall = groupAPI.getSummary(groupId)
|
getActiveGroupIds()
|
||||||
|
}
|
||||||
|
is GetGroupDataTask.Params.FetchWithIds -> {
|
||||||
|
params.groupIds
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val groupRooms = executeRequest<GroupRooms>(eventBus) {
|
Timber.v("Fetch data for group with ids: ${groupIds.joinToString(";")}")
|
||||||
apiCall = groupAPI.getRooms(groupId)
|
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) {
|
insertInDb(data)
|
||||||
apiCall = groupAPI.getUsers(groupId)
|
|
||||||
}
|
|
||||||
insertInDb(groupSummary, groupRooms, groupUsers, groupId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun insertInDb(groupSummary: GroupSummaryResponse,
|
private fun getActiveGroupIds(): List<String> {
|
||||||
groupRooms: GroupRooms,
|
return monarchy.fetchAllMappedSync(
|
||||||
groupUsers: GroupUsers,
|
{ realm ->
|
||||||
groupId: String) {
|
GroupEntity.where(realm, Membership.activeMemberships())
|
||||||
|
},
|
||||||
|
{ it.groupId }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun insertInDb(groupDataList: List<GroupData>) {
|
||||||
monarchy
|
monarchy
|
||||||
.awaitTransaction { realm ->
|
.awaitTransaction { realm ->
|
||||||
val groupSummaryEntity = GroupSummaryEntity.where(realm, groupId).findFirst()
|
groupDataList.forEach { groupData ->
|
||||||
?: realm.createObject(GroupSummaryEntity::class.java, groupId)
|
|
||||||
|
|
||||||
groupSummaryEntity.avatarUrl = groupSummary.profile?.avatarUrl ?: ""
|
val groupSummaryEntity = GroupSummaryEntity.getOrCreate(realm, groupData.groupId)
|
||||||
val name = groupSummary.profile?.name
|
|
||||||
groupSummaryEntity.displayName = if (name.isNullOrEmpty()) groupId else name
|
|
||||||
groupSummaryEntity.shortDescription = groupSummary.profile?.shortDescription ?: ""
|
|
||||||
|
|
||||||
groupSummaryEntity.roomIds.clear()
|
groupSummaryEntity.avatarUrl = groupData.groupSummary.profile?.avatarUrl ?: ""
|
||||||
groupRooms.rooms.mapTo(groupSummaryEntity.roomIds) { it.roomId }
|
val name = groupData.groupSummary.profile?.name
|
||||||
|
groupSummaryEntity.displayName = if (name.isNullOrEmpty()) groupData.groupId else name
|
||||||
|
groupSummaryEntity.shortDescription = groupData.groupSummary.profile?.shortDescription ?: ""
|
||||||
|
|
||||||
groupSummaryEntity.userIds.clear()
|
groupSummaryEntity.roomIds.clear()
|
||||||
groupUsers.users.mapTo(groupSummaryEntity.userIds) { it.userId }
|
groupData.groupRooms.rooms.mapTo(groupSummaryEntity.roomIds) { it.roomId }
|
||||||
|
|
||||||
groupSummaryEntity.membership = when (groupSummary.user?.membership) {
|
groupSummaryEntity.userIds.clear()
|
||||||
Membership.JOIN.value -> Membership.JOIN
|
groupData.groupUsers.users.mapTo(groupSummaryEntity.userIds) { it.userId }
|
||||||
Membership.INVITE.value -> Membership.INVITE
|
|
||||||
else -> Membership.LEAVE
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,20 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.session.group
|
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.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.GroupSummaryQueryParams
|
||||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
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.GroupSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields
|
import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
@ -33,10 +34,15 @@ import io.realm.Realm
|
|||||||
import io.realm.RealmQuery
|
import io.realm.RealmQuery
|
||||||
import javax.inject.Inject
|
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? {
|
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? {
|
override fun getGroupSummary(groupId: String): GroupSummary? {
|
||||||
|
@ -35,7 +35,6 @@ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) :
|
|||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class Params(
|
internal data class Params(
|
||||||
override val sessionId: String,
|
override val sessionId: String,
|
||||||
val groupIds: List<String>,
|
|
||||||
override val lastFailureMessage: String? = null
|
override val lastFailureMessage: String? = null
|
||||||
) : SessionWorkerParams
|
) : SessionWorkerParams
|
||||||
|
|
||||||
@ -48,14 +47,11 @@ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) :
|
|||||||
|
|
||||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||||
sessionComponent.inject(this)
|
sessionComponent.inject(this)
|
||||||
val results = params.groupIds.map { groupId ->
|
return runCatching {
|
||||||
runCatching { fetchGroupData(groupId) }
|
getGroupDataTask.execute(GetGroupDataTask.Params.FetchAllActive)
|
||||||
}
|
}.fold(
|
||||||
val isSuccessful = results.none { it.isFailure }
|
{ Result.success() },
|
||||||
return if (isSuccessful) Result.success() else Result.retry()
|
{ Result.retry() }
|
||||||
}
|
)
|
||||||
|
|
||||||
private suspend fun fetchGroupData(groupId: String) {
|
|
||||||
getGroupDataTask.execute(GetGroupDataTask.Params(groupId))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
@Binds
|
||||||
abstract fun bindGetGroupDataTask(task: DefaultGetGroupDataTask): GetGroupDataTask
|
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
|
@SessionScope
|
||||||
internal class DefaultIdentityService @Inject constructor(
|
internal class DefaultIdentityService @Inject constructor(
|
||||||
private val identityStore: IdentityStore,
|
private val identityStore: IdentityStore,
|
||||||
|
private val ensureIdentityTokenTask: EnsureIdentityTokenTask,
|
||||||
private val getOpenIdTokenTask: GetOpenIdTokenTask,
|
private val getOpenIdTokenTask: GetOpenIdTokenTask,
|
||||||
private val identityBulkLookupTask: IdentityBulkLookupTask,
|
private val identityBulkLookupTask: IdentityBulkLookupTask,
|
||||||
private val identityRegisterTask: IdentityRegisterTask,
|
private val identityRegisterTask: IdentityRegisterTask,
|
||||||
@ -278,7 +279,7 @@ internal class DefaultIdentityService @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun lookUpInternal(canRetry: Boolean, threePids: List<ThreePid>): List<FoundThreePid> {
|
private suspend fun lookUpInternal(canRetry: Boolean, threePids: List<ThreePid>): List<FoundThreePid> {
|
||||||
ensureToken()
|
ensureIdentityTokenTask.execute(Unit)
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
identityBulkLookupTask.execute(IdentityBulkLookupTask.Params(threePids))
|
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 {
|
private suspend fun getNewIdentityServerToken(url: String): String {
|
||||||
val api = retrofitFactory.create(unauthenticatedOkHttpClient, url).create(IdentityAuthAPI::class.java)
|
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
|
@Binds
|
||||||
abstract fun bindIdentityStore(store: RealmIdentityStore): IdentityStore
|
abstract fun bindIdentityStore(store: RealmIdentityStore): IdentityStore
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindEnsureIdentityTokenTask(task: DefaultEnsureIdentityTokenTask): EnsureIdentityTokenTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindIdentityPingTask(task: DefaultIdentityPingTask): IdentityPingTask
|
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.api.util.Cancelable
|
||||||
import im.vector.matrix.android.internal.session.room.directory.GetPublicRoomTask
|
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.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.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class DefaultRoomDirectoryService @Inject constructor(private val getPublicRoomTask: GetPublicRoomTask,
|
internal class DefaultRoomDirectoryService @Inject constructor(private val getPublicRoomTask: GetPublicRoomTask,
|
||||||
private val joinRoomTask: JoinRoomTask,
|
|
||||||
private val getThirdPartyProtocolsTask: GetThirdPartyProtocolsTask,
|
private val getThirdPartyProtocolsTask: GetThirdPartyProtocolsTask,
|
||||||
private val taskExecutor: TaskExecutor) : RoomDirectoryService {
|
private val taskExecutor: TaskExecutor) : RoomDirectoryService {
|
||||||
|
|
||||||
@ -44,14 +42,6 @@ internal class DefaultRoomDirectoryService @Inject constructor(private val getPu
|
|||||||
.executeBy(taskExecutor)
|
.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 {
|
override fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>): Cancelable {
|
||||||
return getThirdPartyProtocolsTask
|
return getThirdPartyProtocolsTask
|
||||||
.configureWith {
|
.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.Room
|
||||||
import im.vector.matrix.android.api.session.room.RoomService
|
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.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.RoomSummary
|
||||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
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.Cancelable
|
||||||
import im.vector.matrix.android.api.util.Optional
|
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.alias.GetRoomIdByAliasTask
|
||||||
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
|
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.membership.joining.JoinRoomTask
|
||||||
import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
|
import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
|
||||||
import im.vector.matrix.android.internal.session.room.summary.RoomSummaryDataSource
|
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 roomIdByAliasTask: GetRoomIdByAliasTask,
|
||||||
private val roomGetter: RoomGetter,
|
private val roomGetter: RoomGetter,
|
||||||
private val roomSummaryDataSource: RoomSummaryDataSource,
|
private val roomSummaryDataSource: RoomSummaryDataSource,
|
||||||
|
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
|
||||||
private val taskExecutor: TaskExecutor
|
private val taskExecutor: TaskExecutor
|
||||||
) : RoomService {
|
) : RoomService {
|
||||||
|
|
||||||
@ -111,4 +114,8 @@ internal class DefaultRoomService @Inject constructor(
|
|||||||
}
|
}
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>> {
|
||||||
|
return roomChangeMembershipStateDataSource.getLiveStates()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,9 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package im.vector.matrix.android.internal.session.room
|
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.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.AggregatedAnnotation
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
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.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.MessagePollResponseContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent
|
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.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.crypto.model.event.EncryptedEventContent
|
||||||
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
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.mapper.EventMapper
|
||||||
import im.vector.matrix.android.internal.database.model.EditAggregatedSummaryEntity
|
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.EventAnnotationsSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
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.PollResponseAggregatedSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntity
|
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields
|
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.create
|
||||||
import im.vector.matrix.android.internal.database.query.getOrCreate
|
import im.vector.matrix.android.internal.database.query.getOrCreate
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
import im.vector.matrix.android.internal.di.UserId
|
||||||
import im.vector.matrix.android.internal.task.Task
|
import im.vector.matrix.android.internal.session.EventInsertLiveProcessor
|
||||||
import im.vector.matrix.android.internal.util.awaitTransaction
|
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal interface EventRelationsAggregationTask : Task<EventRelationsAggregationTask.Params, Unit> {
|
|
||||||
|
|
||||||
data class Params(
|
|
||||||
val events: List<Event>,
|
|
||||||
val userId: String
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class VerificationState {
|
enum class VerificationState {
|
||||||
REQUEST,
|
REQUEST,
|
||||||
WAITING,
|
WAITING,
|
||||||
@ -89,161 +78,145 @@ private fun VerificationState?.toState(newState: VerificationState): Verificatio
|
|||||||
return newState
|
return newState
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String,
|
||||||
* Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base.
|
private val cryptoService: CryptoService
|
||||||
*/
|
) : EventInsertLiveProcessor {
|
||||||
internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
|
||||||
@SessionDatabase private val monarchy: Monarchy,
|
|
||||||
private val cryptoService: CryptoService) : EventRelationsAggregationTask {
|
|
||||||
|
|
||||||
// OPT OUT serer aggregation until API mature enough
|
private val allowedTypes = listOf(
|
||||||
private val SHOULD_HANDLE_SERVER_AGREGGATION = false
|
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) {
|
override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
|
||||||
val events = params.events
|
return allowedTypes.contains(eventType)
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun update(realm: Realm, events: List<Event>, userId: String) {
|
override suspend fun process(realm: Realm, event: Event) {
|
||||||
events.forEach { event ->
|
try { // Temporary catch, should be removed
|
||||||
try { // Temporary catch, should be removed
|
val roomId = event.roomId
|
||||||
val roomId = event.roomId
|
if (roomId == null) {
|
||||||
if (roomId == null) {
|
Timber.w("Event has no room id ${event.eventId}")
|
||||||
Timber.w("Event has no room id ${event.eventId}")
|
return
|
||||||
return@forEach
|
}
|
||||||
|
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 ?: "")
|
EventType.MESSAGE -> {
|
||||||
when (event.type) {
|
if (event.unsignedData?.relations?.annotations != null) {
|
||||||
EventType.REACTION -> {
|
Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}")
|
||||||
// we got a reaction!!
|
handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm)
|
||||||
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)
|
|
||||||
|
|
||||||
EventAnnotationsSummaryEntity.where(realm, event.eventId
|
EventAnnotationsSummaryEntity.where(realm, event.eventId
|
||||||
?: "").findFirst()?.let {
|
?: "").findFirst()?.let {
|
||||||
TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId
|
TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId
|
||||||
?: "").findFirst()?.let { tet ->
|
?: "").findFirst()?.let { tet ->
|
||||||
tet.annotations = it
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EventType.ENCRYPTED -> {
|
val content: MessageContent? = event.content.toModel()
|
||||||
// Relation type is in clear
|
if (content?.relatesTo?.type == RelationType.REPLACE) {
|
||||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
|
||||||
if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE
|
// A replace!
|
||||||
|| encryptedEventContent?.relatesTo?.type == RelationType.RESPONSE
|
handleReplace(realm, event, content, roomId, isLocalEcho)
|
||||||
) {
|
} else if (content?.relatesTo?.type == RelationType.RESPONSE) {
|
||||||
// we need to decrypt if needed
|
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
|
||||||
decryptIfNeeded(event)
|
handleResponse(realm, userId, event, content, roomId, isLocalEcho)
|
||||||
event.getClearContent().toModel<MessageContent>()?.let {
|
}
|
||||||
if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) {
|
}
|
||||||
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
|
|
||||||
// A replace!
|
EventType.KEY_VERIFICATION_DONE,
|
||||||
handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
EventType.KEY_VERIFICATION_CANCEL,
|
||||||
} else if (encryptedEventContent.relatesTo.type == RelationType.RESPONSE) {
|
EventType.KEY_VERIFICATION_ACCEPT,
|
||||||
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
|
EventType.KEY_VERIFICATION_START,
|
||||||
handleResponse(realm, userId, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
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)
|
} else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) {
|
||||||
when (event.getClearType()) {
|
when (event.getClearType()) {
|
||||||
EventType.KEY_VERIFICATION_DONE,
|
EventType.KEY_VERIFICATION_DONE,
|
||||||
EventType.KEY_VERIFICATION_CANCEL,
|
EventType.KEY_VERIFICATION_CANCEL,
|
||||||
EventType.KEY_VERIFICATION_ACCEPT,
|
EventType.KEY_VERIFICATION_ACCEPT,
|
||||||
EventType.KEY_VERIFICATION_START,
|
EventType.KEY_VERIFICATION_START,
|
||||||
EventType.KEY_VERIFICATION_MAC,
|
EventType.KEY_VERIFICATION_MAC,
|
||||||
EventType.KEY_VERIFICATION_READY,
|
EventType.KEY_VERIFICATION_READY,
|
||||||
EventType.KEY_VERIFICATION_KEY -> {
|
EventType.KEY_VERIFICATION_KEY -> {
|
||||||
Timber.v("## SAS REF in room $roomId for event ${event.eventId}")
|
Timber.v("## SAS REF in room $roomId for event ${event.eventId}")
|
||||||
encryptedEventContent.relatesTo.eventId?.let {
|
encryptedEventContent.relatesTo.eventId?.let {
|
||||||
handleVerification(realm, event, roomId, isLocalEcho, it, userId)
|
handleVerification(realm, event, roomId, isLocalEcho, it, userId)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EventType.REDACTION -> {
|
}
|
||||||
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
|
EventType.REDACTION -> {
|
||||||
?: return@forEach
|
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
|
||||||
when (eventToPrune.type) {
|
?: return
|
||||||
EventType.MESSAGE -> {
|
when (eventToPrune.type) {
|
||||||
Timber.d("REDACTION for message ${eventToPrune.eventId}")
|
EventType.MESSAGE -> {
|
||||||
|
Timber.d("REDACTION for message ${eventToPrune.eventId}")
|
||||||
// val unsignedData = EventMapper.map(eventToPrune).unsignedData
|
// val unsignedData = EventMapper.map(eventToPrune).unsignedData
|
||||||
// ?: UnsignedData(null, null)
|
// ?: UnsignedData(null, null)
|
||||||
|
|
||||||
// was this event a m.replace
|
// was this event a m.replace
|
||||||
val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
|
val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
|
||||||
if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
|
if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
|
||||||
handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
|
handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
|
||||||
}
|
|
||||||
}
|
|
||||||
EventType.REACTION -> {
|
|
||||||
handleReactionRedact(eventToPrune, realm, userId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
EventType.REACTION -> {
|
||||||
|
handleReactionRedact(eventToPrune, realm, userId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> Timber.v("UnHandled event ${event.eventId}")
|
|
||||||
}
|
}
|
||||||
} catch (t: Throwable) {
|
else -> Timber.v("UnHandled event ${event.eventId}")
|
||||||
Timber.e(t, "## Should not happen ")
|
|
||||||
}
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Timber.e(t, "## Should not happen ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decryptIfNeeded(event: Event) {
|
// OPT OUT serer aggregation until API mature enough
|
||||||
if (event.mxDecryptionResult == null) {
|
private val SHOULD_HANDLE_SERVER_AGREGGATION = false
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean, relatedEventId: String? = null) {
|
private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean, relatedEventId: String? = null) {
|
||||||
val eventId = event.eventId ?: return
|
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.Content
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
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.PublicRoomsParams
|
||||||
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse
|
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse
|
||||||
import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol
|
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.network.NetworkConstants
|
||||||
import im.vector.matrix.android.internal.session.room.alias.AddRoomAliasBody
|
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.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.RoomMembersResponse
|
||||||
import im.vector.matrix.android.internal.session.room.membership.admin.UserIdAndReason
|
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.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.relation.RelationsResponse
|
||||||
import im.vector.matrix.android.internal.session.room.reporting.ReportContentBody
|
import im.vector.matrix.android.internal.session.room.reporting.ReportContentBody
|
||||||
import im.vector.matrix.android.internal.session.room.send.SendResponse
|
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")
|
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
|
||||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "createRoom")
|
@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.
|
* 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")
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/invite")
|
||||||
fun invite(@Path("roomId") roomId: String, @Body body: InviteBody): Call<Unit>
|
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
|
* 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.joining.JoinRoomTask
|
||||||
import im.vector.matrix.android.internal.session.room.membership.leaving.DefaultLeaveRoomTask
|
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.membership.leaving.LeaveRoomTask
|
||||||
import im.vector.matrix.android.internal.session.room.prune.DefaultPruneEventTask
|
import im.vector.matrix.android.internal.session.room.membership.threepid.DefaultInviteThreePidTask
|
||||||
import im.vector.matrix.android.internal.session.room.prune.PruneEventTask
|
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.DefaultMarkAllRoomsReadTask
|
||||||
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
|
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
|
||||||
import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
|
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.DefaultAddTagToRoomTask
|
||||||
import im.vector.matrix.android.internal.session.room.tags.DefaultDeleteTagFromRoomTask
|
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.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.DefaultGetContextOfEventTask
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask
|
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.GetContextOfEventTask
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
|
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
|
||||||
import im.vector.matrix.android.internal.session.room.typing.DefaultSendTypingTask
|
import im.vector.matrix.android.internal.session.room.typing.DefaultSendTypingTask
|
||||||
@ -129,9 +129,6 @@ internal abstract class RoomModule {
|
|||||||
@Binds
|
@Binds
|
||||||
abstract fun bindFileService(service: DefaultFileService): FileService
|
abstract fun bindFileService(service: DefaultFileService): FileService
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindEventRelationsAggregationTask(task: DefaultEventRelationsAggregationTask): EventRelationsAggregationTask
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindCreateRoomTask(task: DefaultCreateRoomTask): CreateRoomTask
|
abstract fun bindCreateRoomTask(task: DefaultCreateRoomTask): CreateRoomTask
|
||||||
|
|
||||||
@ -144,6 +141,9 @@ internal abstract class RoomModule {
|
|||||||
@Binds
|
@Binds
|
||||||
abstract fun bindInviteTask(task: DefaultInviteTask): InviteTask
|
abstract fun bindInviteTask(task: DefaultInviteTask): InviteTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindInviteThreePidTask(task: DefaultInviteThreePidTask): InviteThreePidTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindJoinRoomTask(task: DefaultJoinRoomTask): JoinRoomTask
|
abstract fun bindJoinRoomTask(task: DefaultJoinRoomTask): JoinRoomTask
|
||||||
|
|
||||||
@ -156,9 +156,6 @@ internal abstract class RoomModule {
|
|||||||
@Binds
|
@Binds
|
||||||
abstract fun bindLoadRoomMembersTask(task: DefaultLoadRoomMembersTask): LoadRoomMembersTask
|
abstract fun bindLoadRoomMembersTask(task: DefaultLoadRoomMembersTask): LoadRoomMembersTask
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindPruneEventTask(task: DefaultPruneEventTask): PruneEventTask
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindSetReadMarkersTask(task: DefaultSetReadMarkersTask): SetReadMarkersTask
|
abstract fun bindSetReadMarkersTask(task: DefaultSetReadMarkersTask): SetReadMarkersTask
|
||||||
|
|
||||||
@ -184,7 +181,7 @@ internal abstract class RoomModule {
|
|||||||
abstract fun bindPaginationTask(task: DefaultPaginationTask): PaginationTask
|
abstract fun bindPaginationTask(task: DefaultPaginationTask): PaginationTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindFetchNextTokenAndPaginateTask(task: DefaultFetchNextTokenAndPaginateTask): FetchNextTokenAndPaginateTask
|
abstract fun bindFetchNextTokenAndPaginateTask(task: DefaultFetchTokenAndPaginateTask): FetchTokenAndPaginateTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindFetchEditHistoryTask(task: DefaultFetchEditHistoryTask): FetchEditHistoryTask
|
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 New Issue
Block a user