Merge branch 'riotx_develop' into rebranding_rebase

* riotx_develop: (111 commits)
  Video calls are shown as a voice ones in the timeline (Fixes #1676)
  Fix regression: not able to create a room without IS configured (Fixes #1679)
  Fix / view attachment crash + freeze when offline
  Version++
  Prepare release 0.91.5
  Fix test compilation issue
  Fix crash after rebase
  Add TODO
  Copy Javadoc to the API class
  Move internal methods to internal task
  Latest renaming
  Rename CreateRoomParamsInternalBuilder to CreateRoomBodyBuilder for clarity
  Rename CreateRoomParamsBuilder to CreateRoomParams for clarity
  Rename internal class
  Expose other objects in the builder to create a room
  ktlint
  Display threePid invite along with the other invite (code is a bit dirty)
  Hide right arrow if threepid invite can not be revoked
  Disable fetching Msisdn, it does not work
  Revoke ThreePid invitation (#548)
  ...

# Conflicts:
#	vector/build.gradle
#	vector/src/main/java/im/vector/riotx/features/crypto/keys/KeysExporter.kt
#	vector/src/main/res/layout/bottom_sheet_logout_and_backup.xml
#	vector/src/main/res/values/strings.xml
This commit is contained in:
Onuray Sahin 2020-07-13 19:59:20 +03:00
commit 797dcdb48b
279 changed files with 7373 additions and 2128 deletions

View File

@ -1,16 +1,15 @@
Changes in Riot.imX 0.91.5 (2020-XX-XX) Changes in Riot.imX 0.91.6 (2020-XX-XX)
=================================================== ===================================================
Features ✨: Features ✨:
- -
Improvements 🙌: 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)
Bugfix 🐛: Bugfix 🐛:
- Regression | Share action menu do not work (#1647) - Video calls are shown as a voice ones in the timeline (#1676)
- Fix regression: not able to create a room without IS configured (#1679)
Translations 🗣: Translations 🗣:
- -
@ -18,12 +17,49 @@ Translations 🗣:
SDK API changes ⚠️: SDK API changes ⚠️:
- -
Build 🧱:
-
Other changes:
-
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 🧱: Build 🧱:
- Upgrade some dependencies - Upgrade some dependencies
- Revert to build-tools 3.5.3 - Revert to build-tools 3.5.3
Other changes: 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) Changes in Riot.imX 0.91.4 (2020-07-06)
=================================================== ===================================================

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

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,78 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
buildscript {
repositories {
maven {
url 'https://jitpack.io'
content {
// PhotoView
includeGroupByRegex 'com\\.github\\.chrisbanes'
}
}
jcenter()
}
}
android {
compileSdkVersion 29
defaultConfig {
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'com.github.chrisbanes:PhotoView:2.0.0'
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.navigation:navigation-fragment-ktx:2.1.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.1.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

View File

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

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

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

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.attachmentviewer
import android.view.View
import android.widget.ImageView
import android.widget.ProgressBar
class AnimatedImageViewHolder constructor(itemView: View) :
BaseViewHolder(itemView) {
val touchImageView: ImageView = itemView.findViewById(R.id.imageView)
val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress)
internal val target = DefaultImageLoaderTarget(this, this.touchImageView)
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.attachmentviewer
sealed class AttachmentEvents {
data class VideoEvent(val isPlaying: Boolean, val progress: Int, val duration: Int) : AttachmentEvents()
}
interface AttachmentEventListener {
fun onEvent(event: AttachmentEvents)
}
sealed class AttachmentCommands {
object PauseVideo : AttachmentCommands()
object StartVideo : AttachmentCommands()
data class SeekTo(val percentProgress: Int) : AttachmentCommands()
}

View File

@ -0,0 +1,45 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.attachmentviewer
import android.content.Context
import android.view.View
sealed class AttachmentInfo(open val uid: String) {
data class Image(override val uid: String, val url: String, val data: Any?) : AttachmentInfo(uid)
data class AnimatedImage(override val uid: String, val url: String, val data: Any?) : AttachmentInfo(uid)
data class Video(override val uid: String, val url: String, val data: Any, val thumbnail: Image?) : AttachmentInfo(uid)
// data class Audio(override val uid: String, val url: String, val data: Any) : AttachmentInfo(uid)
// data class File(override val uid: String, val url: String, val data: Any) : AttachmentInfo(uid)
}
interface AttachmentSourceProvider {
fun getItemCount(): Int
fun getAttachmentInfoAt(position: Int): AttachmentInfo
fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.Image)
fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.AnimatedImage)
fun loadVideo(target: VideoLoaderTarget, info: AttachmentInfo.Video)
fun overlayViewAtPosition(context: Context, position: Int): View?
fun clear(id: String)
}

View File

@ -0,0 +1,335 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright (C) 2018 stfalcon.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.attachmentviewer
import android.graphics.Color
import android.os.Bundle
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GestureDetectorCompat
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.transition.TransitionManager
import androidx.viewpager2.widget.ViewPager2
import kotlinx.android.synthetic.main.activity_attachment_viewer.*
import java.lang.ref.WeakReference
import kotlin.math.abs
abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventListener {
lateinit var pager2: ViewPager2
lateinit var imageTransitionView: ImageView
lateinit var transitionImageContainer: ViewGroup
var topInset = 0
var bottomInset = 0
var systemUiVisibility = true
private var overlayView: View? = null
set(value) {
if (value == overlayView) return
overlayView?.let { rootContainer.removeView(it) }
rootContainer.addView(value)
value?.updatePadding(top = topInset, bottom = bottomInset)
field = value
}
private lateinit var swipeDismissHandler: SwipeToDismissHandler
private lateinit var directionDetector: SwipeDirectionDetector
private lateinit var scaleDetector: ScaleGestureDetector
private lateinit var gestureDetector: GestureDetectorCompat
var currentPosition = 0
private var swipeDirection: SwipeDirection? = null
private fun isScaled() = attachmentsAdapter.isScaled(currentPosition)
private var wasScaled: Boolean = false
private var isSwipeToDismissAllowed: Boolean = true
private lateinit var attachmentsAdapter: AttachmentsAdapter
private var isOverlayWasClicked = false
// private val shouldDismissToBottom: Boolean
// get() = e == null
// || !externalTransitionImageView.isRectVisible
// || !isAtStartPosition
private var isImagePagerIdle = true
fun setSourceProvider(sourceProvider: AttachmentSourceProvider) {
attachmentsAdapter.attachmentSourceProvider = sourceProvider
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// This is important for the dispatchTouchEvent, if not we must correct
// the touch coordinates
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_IMMERSIVE)
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
setContentView(R.layout.activity_attachment_viewer)
attachmentPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL
attachmentsAdapter = AttachmentsAdapter()
attachmentPager.adapter = attachmentsAdapter
imageTransitionView = transitionImageView
transitionImageContainer = findViewById(R.id.transitionImageContainer)
pager2 = attachmentPager
directionDetector = createSwipeDirectionDetector()
gestureDetector = createGestureDetector()
attachmentPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageScrollStateChanged(state: Int) {
isImagePagerIdle = state == ViewPager2.SCROLL_STATE_IDLE
}
override fun onPageSelected(position: Int) {
onSelectedPositionChanged(position)
}
})
swipeDismissHandler = createSwipeToDismissHandler()
rootContainer.setOnTouchListener(swipeDismissHandler)
rootContainer.viewTreeObserver.addOnGlobalLayoutListener { swipeDismissHandler.translationLimit = dismissContainer.height / 4 }
scaleDetector = createScaleGestureDetector()
ViewCompat.setOnApplyWindowInsetsListener(rootContainer) { _, insets ->
overlayView?.updatePadding(top = insets.systemWindowInsetTop, bottom = insets.systemWindowInsetBottom)
topInset = insets.systemWindowInsetTop
bottomInset = insets.systemWindowInsetBottom
insets
}
}
fun onSelectedPositionChanged(position: Int) {
attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition)?.let {
(it as? BaseViewHolder)?.onSelected(false)
}
attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(position)?.let {
(it as? BaseViewHolder)?.onSelected(true)
if (it is VideoViewHolder) {
it.eventListener = WeakReference(this)
}
}
currentPosition = position
overlayView = attachmentsAdapter.attachmentSourceProvider?.overlayViewAtPosition(this@AttachmentViewerActivity, position)
}
override fun onPause() {
attachmentsAdapter.onPause(currentPosition)
super.onPause()
}
override fun onResume() {
super.onResume()
attachmentsAdapter.onResume(currentPosition)
}
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
// The zoomable view is configured to disallow interception when image is zoomed
// Check if the overlay is visible, and wants to handle the click
if (overlayView?.isVisible == true && overlayView?.dispatchTouchEvent(ev) == true) {
return true
}
// Log.v("ATTACHEMENTS", "================\ndispatchTouchEvent $ev")
handleUpDownEvent(ev)
// Log.v("ATTACHEMENTS", "scaleDetector is in progress ${scaleDetector.isInProgress}")
// Log.v("ATTACHEMENTS", "pointerCount ${ev.pointerCount}")
// Log.v("ATTACHEMENTS", "wasScaled $wasScaled")
if (swipeDirection == null && (scaleDetector.isInProgress || ev.pointerCount > 1 || wasScaled)) {
wasScaled = true
// Log.v("ATTACHEMENTS", "dispatch to pager")
return attachmentPager.dispatchTouchEvent(ev)
}
// Log.v("ATTACHEMENTS", "is current item scaled ${isScaled()}")
return (if (isScaled()) super.dispatchTouchEvent(ev) else handleTouchIfNotScaled(ev)).also {
// Log.v("ATTACHEMENTS", "\n================")
}
}
private fun handleUpDownEvent(event: MotionEvent) {
// Log.v("ATTACHEMENTS", "handleUpDownEvent $event")
if (event.action == MotionEvent.ACTION_UP) {
handleEventActionUp(event)
}
if (event.action == MotionEvent.ACTION_DOWN) {
handleEventActionDown(event)
}
scaleDetector.onTouchEvent(event)
gestureDetector.onTouchEvent(event)
}
private fun handleEventActionDown(event: MotionEvent) {
swipeDirection = null
wasScaled = false
attachmentPager.dispatchTouchEvent(event)
swipeDismissHandler.onTouch(rootContainer, event)
isOverlayWasClicked = dispatchOverlayTouch(event)
}
private fun handleEventActionUp(event: MotionEvent) {
// wasDoubleTapped = false
swipeDismissHandler.onTouch(rootContainer, event)
attachmentPager.dispatchTouchEvent(event)
isOverlayWasClicked = dispatchOverlayTouch(event)
}
private fun handleSingleTap(event: MotionEvent, isOverlayWasClicked: Boolean) {
// TODO if there is no overlay, we should at least toggle system bars?
if (overlayView != null && !isOverlayWasClicked) {
toggleOverlayViewVisibility()
super.dispatchTouchEvent(event)
}
}
private fun toggleOverlayViewVisibility() {
if (systemUiVisibility) {
// we hide
TransitionManager.beginDelayedTransition(rootContainer)
hideSystemUI()
overlayView?.isVisible = false
} else {
// we show
TransitionManager.beginDelayedTransition(rootContainer)
showSystemUI()
overlayView?.isVisible = true
}
}
private fun handleTouchIfNotScaled(event: MotionEvent): Boolean {
// Log.v("ATTACHEMENTS", "handleTouchIfNotScaled $event")
directionDetector.handleTouchEvent(event)
return when (swipeDirection) {
SwipeDirection.Up, SwipeDirection.Down -> {
if (isSwipeToDismissAllowed && !wasScaled && isImagePagerIdle) {
swipeDismissHandler.onTouch(rootContainer, event)
} else true
}
SwipeDirection.Left, SwipeDirection.Right -> {
attachmentPager.dispatchTouchEvent(event)
}
else -> true
}
}
private fun handleSwipeViewMove(translationY: Float, translationLimit: Int) {
val alpha = calculateTranslationAlpha(translationY, translationLimit)
backgroundView.alpha = alpha
dismissContainer.alpha = alpha
overlayView?.alpha = alpha
}
private fun dispatchOverlayTouch(event: MotionEvent): Boolean =
overlayView
?.let { it.isVisible && it.dispatchTouchEvent(event) }
?: false
private fun calculateTranslationAlpha(translationY: Float, translationLimit: Int): Float =
1.0f - 1.0f / translationLimit.toFloat() / 4f * abs(translationY)
private fun createSwipeToDismissHandler()
: SwipeToDismissHandler = SwipeToDismissHandler(
swipeView = dismissContainer,
shouldAnimateDismiss = { shouldAnimateDismiss() },
onDismiss = { animateClose() },
onSwipeViewMove = ::handleSwipeViewMove)
private fun createSwipeDirectionDetector() =
SwipeDirectionDetector(this) { swipeDirection = it }
private fun createScaleGestureDetector() =
ScaleGestureDetector(this, ScaleGestureDetector.SimpleOnScaleGestureListener())
private fun createGestureDetector() =
GestureDetectorCompat(this, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
if (isImagePagerIdle) {
handleSingleTap(e, isOverlayWasClicked)
}
return false
}
override fun onDoubleTap(e: MotionEvent?): Boolean {
return super.onDoubleTap(e)
}
})
override fun onEvent(event: AttachmentEvents) {
if (overlayView is AttachmentEventListener) {
(overlayView as? AttachmentEventListener)?.onEvent(event)
}
}
protected open fun shouldAnimateDismiss(): Boolean = true
protected open fun animateClose() {
window.statusBarColor = Color.TRANSPARENT
finish()
}
fun handle(commands: AttachmentCommands) {
(attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition) as? BaseViewHolder)
?.handleCommand(commands)
}
private fun hideSystemUI() {
systemUiVisibility = false
// Enables regular immersive mode.
// For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE.
// Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
// Set the content to appear under the system bars so that the
// content doesn't resize when the system bars hide and show.
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
// Hide the nav bar and status bar
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN)
}
// Shows the system bars by removing all the flags
// except for the ones that make the content appear under the system bars.
private fun showSystemUI() {
systemUiVisibility = true
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
}
}

View File

@ -0,0 +1,115 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.attachmentviewer
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
class AttachmentsAdapter : RecyclerView.Adapter<BaseViewHolder>() {
var attachmentSourceProvider: AttachmentSourceProvider? = null
set(value) {
field = value
notifyDataSetChanged()
}
var recyclerView: RecyclerView? = null
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
this.recyclerView = recyclerView
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
this.recyclerView = null
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
val inflater = LayoutInflater.from(parent.context)
val itemView = inflater.inflate(viewType, parent, false)
return when (viewType) {
R.layout.item_image_attachment -> ZoomableImageViewHolder(itemView)
R.layout.item_animated_image_attachment -> AnimatedImageViewHolder(itemView)
R.layout.item_video_attachment -> VideoViewHolder(itemView)
else -> UnsupportedViewHolder(itemView)
}
}
override fun getItemViewType(position: Int): Int {
val info = attachmentSourceProvider!!.getAttachmentInfoAt(position)
return when (info) {
is AttachmentInfo.Image -> R.layout.item_image_attachment
is AttachmentInfo.Video -> R.layout.item_video_attachment
is AttachmentInfo.AnimatedImage -> R.layout.item_animated_image_attachment
// is AttachmentInfo.Audio -> TODO()
// is AttachmentInfo.File -> TODO()
}
}
override fun getItemCount(): Int {
return attachmentSourceProvider?.getItemCount() ?: 0
}
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
attachmentSourceProvider?.getAttachmentInfoAt(position)?.let {
holder.bind(it)
when (it) {
is AttachmentInfo.Image -> {
attachmentSourceProvider?.loadImage((holder as ZoomableImageViewHolder).target, it)
}
is AttachmentInfo.AnimatedImage -> {
attachmentSourceProvider?.loadImage((holder as AnimatedImageViewHolder).target, it)
}
is AttachmentInfo.Video -> {
attachmentSourceProvider?.loadVideo((holder as VideoViewHolder).target, it)
}
// else -> {
// // }
}
}
}
override fun onViewAttachedToWindow(holder: BaseViewHolder) {
holder.onAttached()
}
override fun onViewRecycled(holder: BaseViewHolder) {
holder.onRecycled()
}
override fun onViewDetachedFromWindow(holder: BaseViewHolder) {
holder.onDetached()
}
fun isScaled(position: Int): Boolean {
val holder = recyclerView?.findViewHolderForAdapterPosition(position)
if (holder is ZoomableImageViewHolder) {
return holder.touchImageView.attacher.scale > 1f
}
return false
}
fun onPause(position: Int) {
val holder = recyclerView?.findViewHolderForAdapterPosition(position) as? BaseViewHolder
holder?.entersBackground()
}
fun onResume(position: Int) {
val holder = recyclerView?.findViewHolderForAdapterPosition(position) as? BaseViewHolder
holder?.entersForeground()
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.attachmentviewer
import android.view.View
import androidx.recyclerview.widget.RecyclerView
abstract class BaseViewHolder constructor(itemView: View) :
RecyclerView.ViewHolder(itemView) {
open fun onRecycled() {
boundResourceUid = null
}
open fun onAttached() {}
open fun onDetached() {}
open fun entersBackground() {}
open fun entersForeground() {}
open fun onSelected(selected: Boolean) {}
open fun handleCommand(commands: AttachmentCommands) {}
var boundResourceUid: String? = null
open fun bind(attachmentInfo: AttachmentInfo) {
boundResourceUid = attachmentInfo.uid
}
}
class UnsupportedViewHolder constructor(itemView: View) :
BaseViewHolder(itemView)

View File

@ -0,0 +1,103 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.attachmentviewer
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
interface ImageLoaderTarget {
fun contextView(): ImageView
fun onResourceLoading(uid: String, placeholder: Drawable?)
fun onLoadFailed(uid: String, errorDrawable: Drawable?)
fun onResourceCleared(uid: String, placeholder: Drawable?)
fun onResourceReady(uid: String, resource: Drawable)
}
internal class DefaultImageLoaderTarget(val holder: AnimatedImageViewHolder, private val contextView: ImageView)
: ImageLoaderTarget {
override fun contextView(): ImageView {
return contextView
}
override fun onResourceLoading(uid: String, placeholder: Drawable?) {
if (holder.boundResourceUid != uid) return
holder.imageLoaderProgress.isVisible = true
}
override fun onLoadFailed(uid: String, errorDrawable: Drawable?) {
if (holder.boundResourceUid != uid) return
holder.imageLoaderProgress.isVisible = false
}
override fun onResourceCleared(uid: String, placeholder: Drawable?) {
if (holder.boundResourceUid != uid) return
holder.touchImageView.setImageDrawable(placeholder)
}
override fun onResourceReady(uid: String, resource: Drawable) {
if (holder.boundResourceUid != uid) return
holder.imageLoaderProgress.isVisible = false
// Glide mess up the view size :/
holder.touchImageView.updateLayoutParams {
width = LinearLayout.LayoutParams.MATCH_PARENT
height = LinearLayout.LayoutParams.MATCH_PARENT
}
holder.touchImageView.setImageDrawable(resource)
if (resource is Animatable) {
resource.start()
}
}
internal class ZoomableImageTarget(val holder: ZoomableImageViewHolder, private val contextView: ImageView) : ImageLoaderTarget {
override fun contextView() = contextView
override fun onResourceLoading(uid: String, placeholder: Drawable?) {
if (holder.boundResourceUid != uid) return
holder.imageLoaderProgress.isVisible = true
}
override fun onLoadFailed(uid: String, errorDrawable: Drawable?) {
if (holder.boundResourceUid != uid) return
holder.imageLoaderProgress.isVisible = false
}
override fun onResourceCleared(uid: String, placeholder: Drawable?) {
if (holder.boundResourceUid != uid) return
holder.touchImageView.setImageDrawable(placeholder)
}
override fun onResourceReady(uid: String, resource: Drawable) {
if (holder.boundResourceUid != uid) return
holder.imageLoaderProgress.isVisible = false
// Glide mess up the view size :/
holder.touchImageView.updateLayoutParams {
width = LinearLayout.LayoutParams.MATCH_PARENT
height = LinearLayout.LayoutParams.MATCH_PARENT
}
holder.touchImageView.setImageDrawable(resource)
}
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright (C) 2018 stfalcon.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.attachmentviewer
sealed class SwipeDirection {
object NotDetected : SwipeDirection()
object Up : SwipeDirection()
object Down : SwipeDirection()
object Left : SwipeDirection()
object Right : SwipeDirection()
companion object {
fun fromAngle(angle: Double): SwipeDirection {
return when (angle) {
in 0.0..45.0 -> Right
in 45.0..135.0 -> Up
in 135.0..225.0 -> Left
in 225.0..315.0 -> Down
in 315.0..360.0 -> Right
else -> NotDetected
}
}
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright (C) 2018 stfalcon.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.attachmentviewer
import android.content.Context
import android.view.MotionEvent
import kotlin.math.sqrt
class SwipeDirectionDetector(
context: Context,
private val onDirectionDetected: (SwipeDirection) -> Unit
) {
private val touchSlop: Int = android.view.ViewConfiguration.get(context).scaledTouchSlop
private var startX: Float = 0f
private var startY: Float = 0f
private var isDetected: Boolean = false
fun handleTouchEvent(event: MotionEvent) {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
startX = event.x
startY = event.y
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
if (!isDetected) {
onDirectionDetected(SwipeDirection.NotDetected)
}
startY = 0.0f
startX = startY
isDetected = false
}
MotionEvent.ACTION_MOVE -> if (!isDetected && getEventDistance(event) > touchSlop) {
isDetected = true
onDirectionDetected(getDirection(startX, startY, event.x, event.y))
}
}
}
/**
* Given two points in the plane p1=(x1, x2) and p2=(y1, y1), this method
* returns the direction that an arrow pointing from p1 to p2 would have.
*
* @param x1 the x position of the first point
* @param y1 the y position of the first point
* @param x2 the x position of the second point
* @param y2 the y position of the second point
* @return the direction
*/
private fun getDirection(x1: Float, y1: Float, x2: Float, y2: Float): SwipeDirection {
val angle = getAngle(x1, y1, x2, y2)
return SwipeDirection.fromAngle(angle)
}
/**
* Finds the angle between two points in the plane (x1,y1) and (x2, y2)
* The angle is measured with 0/360 being the X-axis to the right, angles
* increase counter clockwise.
*
* @param x1 the x position of the first point
* @param y1 the y position of the first point
* @param x2 the x position of the second point
* @param y2 the y position of the second point
* @return the angle between two points
*/
private fun getAngle(x1: Float, y1: Float, x2: Float, y2: Float): Double {
val rad = Math.atan2((y1 - y2).toDouble(), (x2 - x1).toDouble()) + Math.PI
return (rad * 180 / Math.PI + 180) % 360
}
private fun getEventDistance(ev: MotionEvent): Float {
val dx = ev.getX(0) - startX
val dy = ev.getY(0) - startY
return sqrt((dx * dx + dy * dy).toDouble()).toFloat()
}
}

View File

@ -0,0 +1,126 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright (C) 2018 stfalcon.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.attachmentviewer
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint
import android.graphics.Rect
import android.view.MotionEvent
import android.view.View
import android.view.ViewPropertyAnimator
import android.view.animation.AccelerateInterpolator
class SwipeToDismissHandler(
private val swipeView: View,
private val onDismiss: () -> Unit,
private val onSwipeViewMove: (translationY: Float, translationLimit: Int) -> Unit,
private val shouldAnimateDismiss: () -> Boolean
) : View.OnTouchListener {
companion object {
private const val ANIMATION_DURATION = 200L
}
var translationLimit: Int = swipeView.height / 4
private var isTracking = false
private var startY: Float = 0f
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View, event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
if (swipeView.hitRect.contains(event.x.toInt(), event.y.toInt())) {
isTracking = true
}
startY = event.y
return true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
if (isTracking) {
isTracking = false
onTrackingEnd(v.height)
}
return true
}
MotionEvent.ACTION_MOVE -> {
if (isTracking) {
val translationY = event.y - startY
swipeView.translationY = translationY
onSwipeViewMove(translationY, translationLimit)
}
return true
}
else -> {
return false
}
}
}
internal fun initiateDismissToBottom() {
animateTranslation(swipeView.height.toFloat())
}
private fun onTrackingEnd(parentHeight: Int) {
val animateTo = when {
swipeView.translationY < -translationLimit -> -parentHeight.toFloat()
swipeView.translationY > translationLimit -> parentHeight.toFloat()
else -> 0f
}
if (animateTo != 0f && !shouldAnimateDismiss()) {
onDismiss()
} else {
animateTranslation(animateTo)
}
}
private fun animateTranslation(translationTo: Float) {
swipeView.animate()
.translationY(translationTo)
.setDuration(ANIMATION_DURATION)
.setInterpolator(AccelerateInterpolator())
.setUpdateListener { onSwipeViewMove(swipeView.translationY, translationLimit) }
.setAnimatorListener(onAnimationEnd = {
if (translationTo != 0f) {
onDismiss()
}
// remove the update listener, otherwise it will be saved on the next animation execution:
swipeView.animate().setUpdateListener(null)
})
.start()
}
}
internal fun ViewPropertyAnimator.setAnimatorListener(
onAnimationEnd: ((Animator?) -> Unit)? = null,
onAnimationStart: ((Animator?) -> Unit)? = null
) = this.setListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
onAnimationEnd?.invoke(animation)
}
override fun onAnimationStart(animation: Animator?) {
onAnimationStart?.invoke(animation)
}
})
internal val View?.hitRect: Rect
get() = Rect().also { this?.getHitRect(it) }

View File

@ -0,0 +1,76 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.attachmentviewer
import android.graphics.drawable.Drawable
import android.widget.ImageView
import androidx.core.view.isVisible
import java.io.File
interface VideoLoaderTarget {
fun contextView(): ImageView
fun onThumbnailResourceLoading(uid: String, placeholder: Drawable?)
fun onThumbnailLoadFailed(uid: String, errorDrawable: Drawable?)
fun onThumbnailResourceCleared(uid: String, placeholder: Drawable?)
fun onThumbnailResourceReady(uid: String, resource: Drawable)
fun onVideoFileLoading(uid: String)
fun onVideoFileLoadFailed(uid: String)
fun onVideoFileReady(uid: String, file: File)
}
internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val contextView: ImageView) : VideoLoaderTarget {
override fun contextView(): ImageView = contextView
override fun onThumbnailResourceLoading(uid: String, placeholder: Drawable?) {
}
override fun onThumbnailLoadFailed(uid: String, errorDrawable: Drawable?) {
}
override fun onThumbnailResourceCleared(uid: String, placeholder: Drawable?) {
}
override fun onThumbnailResourceReady(uid: String, resource: Drawable) {
if (holder.boundResourceUid != uid) return
holder.thumbnailImage.setImageDrawable(resource)
}
override fun onVideoFileLoading(uid: String) {
if (holder.boundResourceUid != uid) return
holder.thumbnailImage.isVisible = true
holder.loaderProgressBar.isVisible = true
holder.videoView.isVisible = false
}
override fun onVideoFileLoadFailed(uid: String) {
if (holder.boundResourceUid != uid) return
holder.videoFileLoadError()
}
override fun onVideoFileReady(uid: String, file: File) {
if (holder.boundResourceUid != uid) return
holder.thumbnailImage.isVisible = false
holder.loaderProgressBar.isVisible = false
holder.videoView.isVisible = true
holder.videoReady(file)
}
}

View File

@ -0,0 +1,157 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.attachmentviewer
import android.view.View
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.VideoView
import androidx.core.view.isVisible
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import java.io.File
import java.lang.ref.WeakReference
import java.util.concurrent.TimeUnit
// TODO, it would be probably better to use a unique media player
// for better customization and control
// But for now VideoView is enough, it released player when detached, we use a timer to update progress
class VideoViewHolder constructor(itemView: View) :
BaseViewHolder(itemView) {
private var isSelected = false
private var mVideoPath: String? = null
private var progressDisposable: Disposable? = null
private var progress: Int = 0
private var wasPaused = false
var eventListener: WeakReference<AttachmentEventListener>? = null
val thumbnailImage: ImageView = itemView.findViewById(R.id.videoThumbnailImage)
val videoView: VideoView = itemView.findViewById(R.id.videoView)
val loaderProgressBar: ProgressBar = itemView.findViewById(R.id.videoLoaderProgress)
val videoControlIcon: ImageView = itemView.findViewById(R.id.videoControlIcon)
val errorTextView: TextView = itemView.findViewById(R.id.videoMediaViewerErrorView)
internal val target = DefaultVideoLoaderTarget(this, thumbnailImage)
override fun onRecycled() {
super.onRecycled()
progressDisposable?.dispose()
progressDisposable = null
mVideoPath = null
}
fun videoReady(file: File) {
mVideoPath = file.path
if (isSelected) {
startPlaying()
}
}
fun videoFileLoadError() {
}
override fun entersBackground() {
if (videoView.isPlaying) {
progress = videoView.currentPosition
progressDisposable?.dispose()
progressDisposable = null
videoView.stopPlayback()
videoView.pause()
}
}
override fun entersForeground() {
onSelected(isSelected)
}
override fun onSelected(selected: Boolean) {
if (!selected) {
if (videoView.isPlaying) {
progress = videoView.currentPosition
videoView.stopPlayback()
} else {
progress = 0
}
progressDisposable?.dispose()
progressDisposable = null
} else {
if (mVideoPath != null) {
startPlaying()
}
}
isSelected = true
}
private fun startPlaying() {
thumbnailImage.isVisible = false
loaderProgressBar.isVisible = false
videoView.isVisible = true
videoView.setOnPreparedListener {
progressDisposable?.dispose()
progressDisposable = Observable.interval(100, TimeUnit.MILLISECONDS)
.timeInterval()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
val duration = videoView.duration
val progress = videoView.currentPosition
val isPlaying = videoView.isPlaying
// Log.v("FOO", "isPlaying $isPlaying $progress/$duration")
eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration))
}
}
videoView.setVideoPath(mVideoPath)
if (!wasPaused) {
videoView.start()
if (progress > 0) {
videoView.seekTo(progress)
}
}
}
override fun handleCommand(commands: AttachmentCommands) {
if (!isSelected) return
when (commands) {
AttachmentCommands.StartVideo -> {
wasPaused = false
videoView.start()
}
AttachmentCommands.PauseVideo -> {
wasPaused = true
videoView.pause()
}
is AttachmentCommands.SeekTo -> {
val duration = videoView.duration
if (duration > 0) {
val seekDuration = duration * (commands.percentProgress / 100f)
videoView.seekTo(seekDuration.toInt())
}
}
}
}
override fun bind(attachmentInfo: AttachmentInfo) {
super.bind(attachmentInfo)
progress = 0
wasPaused = false
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.attachmentviewer
import android.view.View
import android.widget.ProgressBar
import com.github.chrisbanes.photoview.PhotoView
class ZoomableImageViewHolder constructor(itemView: View) :
BaseViewHolder(itemView) {
val touchImageView: PhotoView = itemView.findViewById(R.id.touchImageView)
val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress)
init {
touchImageView.setAllowParentInterceptOnEdge(false)
touchImageView.setOnScaleChangeListener { scaleFactor, _, _ ->
// Log.v("ATTACHEMENTS", "scaleFactor $scaleFactor")
// It's a bit annoying but when you pitch down the scaling
// is not exactly one :/
touchImageView.setAllowParentInterceptOnEdge(scaleFactor <= 1.0008f)
}
touchImageView.setScale(1.0f, true)
touchImageView.setAllowParentInterceptOnEdge(true)
}
internal val target = DefaultImageLoaderTarget.ZoomableImageTarget(this, touchImageView)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,7 +33,7 @@ data class MatrixConfiguration(
), ),
/** /**
* Optional proxy to connect to the matrix servers * Optional proxy to connect to the matrix servers
* You can create one using for instance Proxy(proxyType, InetSocketAddress(hostname, port) * You can create one using for instance Proxy(proxyType, InetSocketAddress.createUnresolved(hostname, port)
*/ */
val proxy: Proxy? = null val proxy: Proxy? = null
) { ) {

View File

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

View File

@ -47,6 +47,7 @@ import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.session.typing.TypingUsersTracker import im.vector.matrix.android.api.session.typing.TypingUsersTracker
import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.api.session.widgets.WidgetService import im.vector.matrix.android.api.session.widgets.WidgetService
import okhttp3.OkHttpClient
/** /**
* This interface defines interactions with a session. * This interface defines interactions with a session.
@ -205,6 +206,13 @@ interface Session :
*/ */
fun removeListener(listener: Listener) fun removeListener(listener: Listener)
/**
* Will return a OkHttpClient which will manage pinned certificates and Proxy if configured.
* It will not add any access-token to the request.
* So it is exposed to let the app be able to download image with Glide or any other libraries which accept an OkHttp client.
*/
fun getOkHttpClient(): OkHttpClient
/** /**
* A global session listener to get notified for some events. * A global session listener to get notified for some events.
*/ */

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -59,5 +59,5 @@ data class CallInviteContent(
} }
} }
fun isVideo(): Boolean = offer?.sdp?.contains(Offer.SDP_VIDEO) == true fun isVideo() = offer?.sdp?.contains(Offer.SDP_VIDEO) == true
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -72,7 +72,7 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
it.process(realm, domainEvent) it.process(realm, domainEvent)
} }
} }
realm.where(EventInsertEntity::class.java).findAll().deleteAllFromRealm() realm.delete(EventInsertEntity::class.java)
} }
} }
} }
@ -88,8 +88,8 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
) )
} catch (e: MXCryptoError) { } catch (e: MXCryptoError) {
Timber.v("Call service: Failed to decrypt event") Timber.v("Failed to decrypt event")
// TODO -> we should keep track of this and retry, or aggregation will be broken // TODO -> we should keep track of this and retry, or some processing will never be handled
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -109,7 +109,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
return return
} }
val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "") val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "")
when (event.getClearType()) { when (event.type) {
EventType.REACTION -> { EventType.REACTION -> {
// we got a reaction!! // we got a reaction!!
Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}") Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}")
@ -161,7 +161,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE
|| encryptedEventContent?.relatesTo?.type == RelationType.RESPONSE || encryptedEventContent?.relatesTo?.type == RelationType.RESPONSE
) { ) {
// we need to decrypt if needed
event.getClearContent().toModel<MessageContent>()?.let { event.getClearContent().toModel<MessageContent>()?.let {
if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) { if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) {
Timber.v("###REPLACE in room $roomId for event ${event.eventId}") Timber.v("###REPLACE in room $roomId for event ${event.eventId}")

View File

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

View File

@ -44,6 +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.membership.threepid.DefaultInviteThreePidTask
import im.vector.matrix.android.internal.session.room.membership.threepid.InviteThreePidTask
import im.vector.matrix.android.internal.session.room.read.DefaultMarkAllRoomsReadTask import im.vector.matrix.android.internal.session.room.read.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
@ -139,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

View File

@ -0,0 +1,115 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.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?
)

View File

@ -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 { invites ->
// 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
invites.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() }
}
}
}
}
}
}

View File

@ -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.
@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.matrix.android.api.session.room.model.create package im.vector.matrix.android.internal.session.room.create
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass

View File

@ -17,11 +17,9 @@
package im.vector.matrix.android.internal.session.room.create package im.vector.matrix.android.internal.session.room.create
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure
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.room.model.create.CreateRoomResponse import im.vector.matrix.android.api.session.room.model.create.CreateRoomPreset
import im.vector.matrix.android.internal.crypto.DeviceListManager
import im.vector.matrix.android.internal.database.awaitNotEmptyResult import im.vector.matrix.android.internal.database.awaitNotEmptyResult
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomEntityFields import im.vector.matrix.android.internal.database.model.RoomEntityFields
@ -51,20 +49,15 @@ internal class DefaultCreateRoomTask @Inject constructor(
private val readMarkersTask: SetReadMarkersTask, private val readMarkersTask: SetReadMarkersTask,
@SessionDatabase @SessionDatabase
private val realmConfiguration: RealmConfiguration, private val realmConfiguration: RealmConfiguration,
private val crossSigningService: CrossSigningService, private val createRoomBodyBuilder: CreateRoomBodyBuilder,
private val deviceListManager: DeviceListManager,
private val eventBus: EventBus private val eventBus: EventBus
) : CreateRoomTask { ) : CreateRoomTask {
override suspend fun execute(params: CreateRoomParams): String { override suspend fun execute(params: CreateRoomParams): String {
val createRoomParams = if (canEnableEncryption(params)) { val createRoomBody = createRoomBodyBuilder.build(params)
params.enableEncryptionWithAlgorithm()
} else {
params
}
val createRoomResponse = executeRequest<CreateRoomResponse>(eventBus) { val createRoomResponse = executeRequest<CreateRoomResponse>(eventBus) {
apiCall = roomAPI.createRoom(createRoomParams) apiCall = roomAPI.createRoom(createRoomBody)
} }
val roomId = createRoomResponse.roomId val roomId = createRoomResponse.roomId
// Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before) // Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before)
@ -76,35 +69,13 @@ internal class DefaultCreateRoomTask @Inject constructor(
} catch (exception: TimeoutCancellationException) { } catch (exception: TimeoutCancellationException) {
throw CreateRoomFailure.CreatedWithTimeout throw CreateRoomFailure.CreatedWithTimeout
} }
if (createRoomParams.isDirect()) { if (params.isDirect()) {
handleDirectChatCreation(createRoomParams, roomId) handleDirectChatCreation(params, roomId)
} }
setReadMarkers(roomId) setReadMarkers(roomId)
return roomId return roomId
} }
private suspend fun canEnableEncryption(params: CreateRoomParams): Boolean {
return params.enableEncryptionIfInvitedUsersSupportIt
&& crossSigningService.isCrossSigningVerified()
&& params.invite3pids.isNullOrEmpty()
&& params.invitedUserIds?.isNotEmpty() == true
&& 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() }
}
}
}
}
}
private suspend fun handleDirectChatCreation(params: CreateRoomParams, roomId: String) { private suspend fun handleDirectChatCreation(params: CreateRoomParams, roomId: String) {
val otherUserId = params.getFirstInvitedUserId() val otherUserId = params.getFirstInvitedUserId()
?: throw IllegalStateException("You can't create a direct room without an invitedUser") ?: throw IllegalStateException("You can't create a direct room without an invitedUser")
@ -123,4 +94,21 @@ internal class DefaultCreateRoomTask @Inject constructor(
val setReadMarkerParams = SetReadMarkersTask.Params(roomId, forceReadReceipt = true, forceReadMarker = true) val setReadMarkerParams = SetReadMarkersTask.Params(roomId, forceReadReceipt = true, forceReadMarker = true)
return readMarkersTask.execute(setReadMarkerParams) return readMarkersTask.execute(setReadMarkerParams)
} }
/**
* Tells if the created room can be a direct chat one.
*
* @return true if it is a direct chat
*/
private fun CreateRoomParams.isDirect(): Boolean {
return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
&& isDirect == true
}
/**
* @return the first invited user id
*/
private fun CreateRoomParams.getFirstInvitedUserId(): String? {
return invitedUserIds.firstOrNull() ?: invite3pids.firstOrNull()?.value
}
} }

View File

@ -21,6 +21,7 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.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.members.MembershipService import im.vector.matrix.android.api.session.room.members.MembershipService
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.Membership import im.vector.matrix.android.api.session.room.model.Membership
@ -36,6 +37,7 @@ import im.vector.matrix.android.internal.session.room.membership.admin.Membershi
import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask
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.LeaveRoomTask import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask
import im.vector.matrix.android.internal.session.room.membership.threepid.InviteThreePidTask
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 im.vector.matrix.android.internal.util.fetchCopied import im.vector.matrix.android.internal.util.fetchCopied
@ -48,6 +50,7 @@ internal class DefaultMembershipService @AssistedInject constructor(
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val loadRoomMembersTask: LoadRoomMembersTask, private val loadRoomMembersTask: LoadRoomMembersTask,
private val inviteTask: InviteTask, private val inviteTask: InviteTask,
private val inviteThreePidTask: InviteThreePidTask,
private val joinTask: JoinRoomTask, private val joinTask: JoinRoomTask,
private val leaveRoomTask: LeaveRoomTask, private val leaveRoomTask: LeaveRoomTask,
private val membershipAdminTask: MembershipAdminTask, private val membershipAdminTask: MembershipAdminTask,
@ -152,6 +155,15 @@ internal class DefaultMembershipService @AssistedInject constructor(
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun invite3pid(threePid: ThreePid, callback: MatrixCallback<Unit>): Cancelable {
val params = InviteThreePidTask.Params(roomId, threePid)
return inviteThreePidTask
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun join(reason: String?, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable { override fun join(reason: String?, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable {
val params = JoinRoomTask.Params(roomId, reason, viaServers) val params = JoinRoomTask.Params(roomId, reason, viaServers)
return joinTask return joinTask

View File

@ -0,0 +1,67 @@
/*
* 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.membership
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.internal.session.SessionScope
import javax.inject.Inject
/**
* This class holds information about rooms that current user is joining or leaving.
*/
@SessionScope
internal class RoomChangeMembershipStateDataSource @Inject constructor() {
private val mutableLiveStates = MutableLiveData<Map<String, ChangeMembershipState>>(emptyMap())
private val states = HashMap<String, ChangeMembershipState>()
/**
* This will update local states to be synced with the server.
*/
fun setMembershipFromSync(roomId: String, membership: Membership) {
if (states.containsKey(roomId)) {
val newState = membership.toMembershipChangeState()
updateState(roomId, newState)
}
}
fun updateState(roomId: String, state: ChangeMembershipState) {
states[roomId] = state
mutableLiveStates.postValue(states.toMap())
}
fun getLiveStates(): LiveData<Map<String, ChangeMembershipState>> {
return mutableLiveStates
}
fun getState(roomId: String): ChangeMembershipState {
return states.getOrElse(roomId) {
ChangeMembershipState.Unknown
}
}
private fun Membership.toMembershipChangeState(): ChangeMembershipState {
return when {
this == Membership.JOIN -> ChangeMembershipState.Joined
this.isLeft() -> ChangeMembershipState.Left
else -> ChangeMembershipState.Unknown
}
}
}

View File

@ -30,8 +30,15 @@ internal class RoomMemberEventHandler @Inject constructor() {
if (event.type != EventType.STATE_ROOM_MEMBER) { if (event.type != EventType.STATE_ROOM_MEMBER) {
return false return false
} }
val roomMember = event.content.toModel<RoomMemberContent>() ?: return false
val userId = event.stateKey ?: return false val userId = event.stateKey ?: return false
val roomMember = event.content.toModel<RoomMemberContent>()
return handle(realm, roomId, userId, roomMember)
}
fun handle(realm: Realm, roomId: String, userId: String, roomMember: RoomMemberContent?): Boolean {
if (roomMember == null) {
return false
}
val roomMemberEntity = RoomMemberEntityFactory.create(roomId, userId, roomMember) val roomMemberEntity = RoomMemberEntityFactory.create(roomId, userId, roomMember)
realm.insertOrUpdate(roomMemberEntity) realm.insertOrUpdate(roomMemberEntity)
if (roomMember.membership.isActive()) { if (roomMember.membership.isActive()) {

View File

@ -17,13 +17,15 @@
package im.vector.matrix.android.internal.session.room.membership.joining package im.vector.matrix.android.internal.session.room.membership.joining
import im.vector.matrix.android.api.session.room.failure.JoinRoomFailure import im.vector.matrix.android.api.session.room.failure.JoinRoomFailure
import im.vector.matrix.android.api.session.room.model.create.JoinRoomResponse import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.internal.database.awaitNotEmptyResult import im.vector.matrix.android.internal.database.awaitNotEmptyResult
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomEntityFields import im.vector.matrix.android.internal.database.model.RoomEntityFields
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
import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.session.room.create.JoinRoomResponse
import im.vector.matrix.android.internal.session.room.membership.RoomChangeMembershipStateDataSource
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
@ -45,12 +47,19 @@ internal class DefaultJoinRoomTask @Inject constructor(
private val readMarkersTask: SetReadMarkersTask, private val readMarkersTask: SetReadMarkersTask,
@SessionDatabase @SessionDatabase
private val realmConfiguration: RealmConfiguration, private val realmConfiguration: RealmConfiguration,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
private val eventBus: EventBus private val eventBus: EventBus
) : JoinRoomTask { ) : JoinRoomTask {
override suspend fun execute(params: JoinRoomTask.Params) { override suspend fun execute(params: JoinRoomTask.Params) {
val joinRoomResponse = executeRequest<JoinRoomResponse>(eventBus) { roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.Joining)
apiCall = roomAPI.join(params.roomIdOrAlias, params.viaServers, mapOf("reason" to params.reason)) val joinRoomResponse = try {
executeRequest<JoinRoomResponse>(eventBus) {
apiCall = roomAPI.join(params.roomIdOrAlias, params.viaServers, mapOf("reason" to params.reason))
}
} catch (failure: Throwable) {
roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.FailedJoining(failure))
throw failure
} }
// Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before) // Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before)
val roomId = joinRoomResponse.roomId val roomId = joinRoomResponse.roomId

View File

@ -16,10 +16,19 @@
package im.vector.matrix.android.internal.session.room.membership.leaving package im.vector.matrix.android.internal.session.room.membership.leaving
import im.vector.matrix.android.api.query.QueryStringValue
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.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.session.room.membership.RoomChangeMembershipStateDataSource
import im.vector.matrix.android.internal.session.room.state.StateEventDataSource
import im.vector.matrix.android.internal.session.room.summary.RoomSummaryDataSource
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal interface LeaveRoomTask : Task<LeaveRoomTask.Params, Unit> { internal interface LeaveRoomTask : Task<LeaveRoomTask.Params, Unit> {
@ -31,12 +40,40 @@ internal interface LeaveRoomTask : Task<LeaveRoomTask.Params, Unit> {
internal class DefaultLeaveRoomTask @Inject constructor( internal class DefaultLeaveRoomTask @Inject constructor(
private val roomAPI: RoomAPI, private val roomAPI: RoomAPI,
private val eventBus: EventBus private val eventBus: EventBus,
private val stateEventDataSource: StateEventDataSource,
private val roomSummaryDataSource: RoomSummaryDataSource,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource
) : LeaveRoomTask { ) : LeaveRoomTask {
override suspend fun execute(params: LeaveRoomTask.Params) { override suspend fun execute(params: LeaveRoomTask.Params) {
return executeRequest(eventBus) { leaveRoom(params.roomId, params.reason)
apiCall = roomAPI.leave(params.roomId, mapOf("reason" to params.reason)) }
private suspend fun leaveRoom(roomId: String, reason: String?) {
val roomSummary = roomSummaryDataSource.getRoomSummary(roomId)
if (roomSummary?.membership?.isActive() == false) {
Timber.v("Room $roomId is not joined so can't be left")
return
}
roomChangeMembershipStateDataSource.updateState(roomId, ChangeMembershipState.Leaving)
val roomCreateStateEvent = stateEventDataSource.getStateEvent(
roomId = roomId,
eventType = EventType.STATE_ROOM_CREATE,
stateKey = QueryStringValue.NoCondition
)
// Server is not cleaning predecessor rooms, so we also try to left them
val predecessorRoomId = roomCreateStateEvent?.getClearContent()?.toModel<RoomCreateContent>()?.predecessor?.roomId
if (predecessorRoomId != null) {
leaveRoom(predecessorRoomId, reason)
}
try {
executeRequest<Unit>(eventBus) {
apiCall = roomAPI.leave(roomId, mapOf("reason" to reason))
}
} catch (failure: Throwable) {
roomChangeMembershipStateDataSource.updateState(roomId, ChangeMembershipState.FailedLeaving(failure))
throw failure
} }
} }
} }

View File

@ -0,0 +1,65 @@
/*
* 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.membership.threepid
import im.vector.matrix.android.api.session.identity.IdentityServiceError
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.api.session.identity.toMedium
import im.vector.matrix.android.internal.di.AuthenticatedIdentity
import im.vector.matrix.android.internal.network.executeRequest
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.RoomAPI
import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface InviteThreePidTask : Task<InviteThreePidTask.Params, Unit> {
data class Params(
val roomId: String,
val threePid: ThreePid
)
}
internal class DefaultInviteThreePidTask @Inject constructor(
private val roomAPI: RoomAPI,
private val eventBus: EventBus,
private val identityStore: IdentityStore,
private val ensureIdentityTokenTask: EnsureIdentityTokenTask,
@AuthenticatedIdentity
private val accessTokenProvider: AccessTokenProvider
) : InviteThreePidTask {
override suspend fun execute(params: InviteThreePidTask.Params) {
ensureIdentityTokenTask.execute(Unit)
val identityServerUrlWithoutProtocol = identityStore.getIdentityServerUrlWithoutProtocol() ?: throw IdentityServiceError.NoIdentityServerConfigured
val identityServerAccessToken = accessTokenProvider.getToken() ?: throw IdentityServiceError.NoIdentityServerConfigured
return executeRequest(eventBus) {
val body = ThreePidInviteBody(
id_server = identityServerUrlWithoutProtocol,
id_access_token = identityServerAccessToken,
medium = params.threePid.toMedium(),
address = params.threePid.value
)
apiCall = roomAPI.invite3pid(params.roomId, body)
}
}
}

View File

@ -0,0 +1,41 @@
/*
* 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.membership.threepid
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class ThreePidInviteBody(
/**
* Required. The hostname+port of the identity server which should be used for third party identifier lookups.
*/
@Json(name = "id_server") val id_server: String,
/**
* Required. An access token previously registered with the identity server. Servers can treat this as optional
* to distinguish between r0.5-compatible clients and this specification version.
*/
@Json(name = "id_access_token") val id_access_token: String,
/**
* Required. The kind of address being passed in the address field, for example email.
*/
@Json(name = "medium") val medium: String,
/**
* Required. The invitee's third party identifier.
*/
@Json(name = "address") val address: String
)

View File

@ -100,6 +100,7 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
private fun roomSummariesQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery<RoomSummaryEntity> { private fun roomSummariesQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery<RoomSummaryEntity> {
val query = RoomSummaryEntity.where(realm) val query = RoomSummaryEntity.where(realm)
query.process(RoomSummaryEntityFields.ROOM_ID, queryParams.roomId)
query.process(RoomSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) query.process(RoomSummaryEntityFields.DISPLAY_NAME, queryParams.displayName)
query.process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias) query.process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias)
query.process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) query.process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships)

View File

@ -349,7 +349,7 @@ internal class DefaultTimeline(
updateState(Timeline.Direction.FORWARDS) { updateState(Timeline.Direction.FORWARDS) {
it.copy( it.copy(
hasMoreInCache = firstBuiltEvent == null || firstBuiltEvent.displayIndex < firstCacheEvent?.displayIndex ?: Int.MIN_VALUE, hasMoreInCache = firstBuiltEvent != null && firstBuiltEvent.displayIndex < firstCacheEvent?.displayIndex ?: Int.MIN_VALUE,
hasReachedEnd = chunkEntity?.isLastForward ?: false hasReachedEnd = chunkEntity?.isLastForward ?: false
) )
} }
@ -369,6 +369,9 @@ internal class DefaultTimeline(
private fun paginateInternal(startDisplayIndex: Int?, private fun paginateInternal(startDisplayIndex: Int?,
direction: Timeline.Direction, direction: Timeline.Direction,
count: Int): Boolean { count: Int): Boolean {
if (count == 0) {
return false
}
updateState(direction) { it.copy(requestedPaginationCount = count, isPaginating = true) } updateState(direction) { it.copy(requestedPaginationCount = count, isPaginating = true) }
val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong()) val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong())
val shouldFetchMore = builtCount < count && !hasReachedEnd(direction) val shouldFetchMore = builtCount < count && !hasReachedEnd(direction)

View File

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

View File

@ -241,12 +241,13 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
chunksToDelete.add(it) chunksToDelete.add(it)
} }
} }
val shouldUpdateSummary = chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS
chunksToDelete.forEach { chunksToDelete.forEach {
it.deleteOnCascade() it.deleteOnCascade()
} }
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
val shouldUpdateSummary = roomSummaryEntity.latestPreviewableEvent == null
|| (chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS)
if (shouldUpdateSummary) { if (shouldUpdateSummary) {
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
val latestPreviewableEvent = TimelineEventEntity.latestEvent( val latestPreviewableEvent = TimelineEventEntity.latestEvent(
realm, realm,
roomId, roomId,

View File

@ -31,7 +31,7 @@ import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResu
import im.vector.matrix.android.internal.database.helper.addOrUpdate import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.helper.addTimelineEvent import im.vector.matrix.android.internal.database.helper.addTimelineEvent
import im.vector.matrix.android.internal.database.helper.deleteOnCascade import im.vector.matrix.android.internal.database.helper.deleteOnCascade
import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.mapper.toEntity import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
@ -48,6 +48,7 @@ import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
import im.vector.matrix.android.internal.session.mapWithProgress import im.vector.matrix.android.internal.session.mapWithProgress
import im.vector.matrix.android.internal.session.room.membership.RoomChangeMembershipStateDataSource
import im.vector.matrix.android.internal.session.room.membership.RoomMemberEventHandler import im.vector.matrix.android.internal.session.room.membership.RoomMemberEventHandler
import im.vector.matrix.android.internal.session.room.read.FullyReadContent import im.vector.matrix.android.internal.session.room.read.FullyReadContent
import im.vector.matrix.android.internal.session.room.summary.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.summary.RoomSummaryUpdater
@ -73,6 +74,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
private val cryptoService: DefaultCryptoService, private val cryptoService: DefaultCryptoService,
private val roomMemberEventHandler: RoomMemberEventHandler, private val roomMemberEventHandler: RoomMemberEventHandler,
private val roomTypingUsersHandler: RoomTypingUsersHandler, private val roomTypingUsersHandler: RoomTypingUsersHandler,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
@UserId private val userId: String, @UserId private val userId: String,
private val eventBus: EventBus, private val eventBus: EventBus,
private val timelineEventDecryptor: TimelineEventDecryptor) { private val timelineEventDecryptor: TimelineEventDecryptor) {
@ -185,6 +187,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
} != null } != null
roomTypingUsersHandler.handle(realm, roomId, ephemeralResult) roomTypingUsersHandler.handle(realm, roomId, ephemeralResult)
roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.JOIN)
roomSummaryUpdater.update( roomSummaryUpdater.update(
realm, realm,
roomId, roomId,
@ -221,6 +224,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
val inviterEvent = roomSync.inviteState?.events?.lastOrNull { val inviterEvent = roomSync.inviteState?.events?.lastOrNull {
it.type == EventType.STATE_ROOM_MEMBER it.type == EventType.STATE_ROOM_MEMBER
} }
roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.INVITE)
roomSummaryUpdater.update(realm, roomId, Membership.INVITE, updateMembers = true, inviterId = inviterEvent?.senderId) roomSummaryUpdater.update(realm, roomId, Membership.INVITE, updateMembers = true, inviterId = inviterEvent?.senderId)
return roomEntity return roomEntity
} }
@ -263,6 +267,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
val membership = leftMember?.membership ?: Membership.LEAVE val membership = leftMember?.membership ?: Membership.LEAVE
roomEntity.membership = membership roomEntity.membership = membership
roomEntity.chunks.deleteAllFromRealm() roomEntity.chunks.deleteAllFromRealm()
roomTypingUsersHandler.handle(realm, roomId, null)
roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.LEAVE)
roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary, roomSync.unreadNotifications) roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary, roomSync.unreadNotifications)
return roomEntity return roomEntity
} }
@ -307,14 +313,15 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
root = eventEntity root = eventEntity
} }
if (event.type == EventType.STATE_ROOM_MEMBER) { if (event.type == EventType.STATE_ROOM_MEMBER) {
roomMemberContentsByUser[event.stateKey] = event.content.toModel() val fixedContent = event.getFixedRoomMemberContent()
roomMemberEventHandler.handle(realm, roomEntity.roomId, event) roomMemberContentsByUser[event.stateKey] = fixedContent
roomMemberEventHandler.handle(realm, roomEntity.roomId, event.stateKey, fixedContent)
} }
} }
roomMemberContentsByUser.getOrPut(event.senderId) { roomMemberContentsByUser.getOrPut(event.senderId) {
// If we don't have any new state on this user, get it from db // If we don't have any new state on this user, get it from db
val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root
ContentMapper.map(rootStateEvent?.content).toModel() rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
} }
chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser) chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
@ -405,4 +412,18 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
} }
} }
} }
private fun Event.getFixedRoomMemberContent(): RoomMemberContent? {
val content = content.toModel<RoomMemberContent>()
// if user is leaving, we should grab his last name and avatar from prevContent
return if (content?.membership?.isLeft() == true) {
val prevContent = resolvedPrevContent().toModel<RoomMemberContent>()
content.copy(
displayName = prevContent?.displayName,
avatarUrl = prevContent?.avatarUrl
)
} else {
content
}
}
} }

View File

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

View File

@ -279,6 +279,7 @@ dependencies {
implementation project(":matrix-sdk-android-rx") implementation project(":matrix-sdk-android-rx")
implementation project(":diff-match-patch") implementation project(":diff-match-patch")
implementation project(":multipicker") implementation project(":multipicker")
implementation project(":attachment-viewer")
implementation 'com.android.support:multidex:1.0.3' implementation 'com.android.support:multidex:1.0.3'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
@ -289,7 +290,8 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.appcompat:appcompat:1.1.0'
implementation "androidx.fragment:fragment:$fragment_version" implementation "androidx.fragment:fragment:$fragment_version"
implementation "androidx.fragment:fragment-ktx:$fragment_version" implementation "androidx.fragment:fragment-ktx:$fragment_version"
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta7' // Keep at 2.0.0-beta4 at the moment, as updating is breaking some UI
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
implementation 'androidx.core:core-ktx:1.3.0' implementation 'androidx.core:core-ktx:1.3.0'
implementation "org.threeten:threetenbp:1.4.0:no-tzdb" implementation "org.threeten:threetenbp:1.4.0:no-tzdb"
@ -368,6 +370,10 @@ dependencies {
implementation "com.github.piasy:GlideImageLoader:$big_image_viewer_version" implementation "com.github.piasy:GlideImageLoader:$big_image_viewer_version"
implementation "com.github.piasy:ProgressPieIndicator:$big_image_viewer_version" implementation "com.github.piasy:ProgressPieIndicator:$big_image_viewer_version"
implementation "com.github.piasy:GlideImageViewFactory:$big_image_viewer_version" implementation "com.github.piasy:GlideImageViewFactory:$big_image_viewer_version"
// implementation 'com.github.MikeOrtiz:TouchImageView:3.0.2'
implementation 'com.github.chrisbanes:PhotoView:2.0.0'
implementation "com.github.bumptech.glide:glide:$glide_version" implementation "com.github.bumptech.glide:glide:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version" kapt "com.github.bumptech.glide:compiler:$glide_version"
implementation 'com.danikula:videocache:2.7.1' implementation 'com.danikula:videocache:2.7.1'

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,155 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.core.contacts
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.ContactsContract
import androidx.annotation.WorkerThread
import timber.log.Timber
import javax.inject.Inject
import kotlin.system.measureTimeMillis
class ContactsDataSource @Inject constructor(
private val context: Context
) {
/**
* Will return a list of contact from the contacts book of the device, with at least one email or phone.
* If both param are false, you will get en empty list.
* Note: The return list does not contain any matrixId.
*/
@WorkerThread
fun getContacts(
withEmails: Boolean,
withMsisdn: Boolean
): List<MappedContact> {
val map = mutableMapOf<Long, MappedContactBuilder>()
val contentResolver = context.contentResolver
measureTimeMillis {
contentResolver.query(
ContactsContract.Contacts.CONTENT_URI,
arrayOf(
ContactsContract.Contacts._ID,
ContactsContract.Data.DISPLAY_NAME,
ContactsContract.Data.PHOTO_URI
),
null,
null,
// Sort by Display name
ContactsContract.Data.DISPLAY_NAME
)
?.use { cursor ->
if (cursor.count > 0) {
while (cursor.moveToNext()) {
val id = cursor.getLong(ContactsContract.Contacts._ID) ?: continue
val displayName = cursor.getString(ContactsContract.Contacts.DISPLAY_NAME) ?: continue
val mappedContactBuilder = MappedContactBuilder(
id = id,
displayName = displayName
)
cursor.getString(ContactsContract.Data.PHOTO_URI)
?.let { Uri.parse(it) }
?.let { mappedContactBuilder.photoURI = it }
map[id] = mappedContactBuilder
}
}
}
// Get the phone numbers
if (withMsisdn) {
contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
arrayOf(
ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
ContactsContract.CommonDataKinds.Phone.NUMBER
),
null,
null,
null)
?.use { innerCursor ->
while (innerCursor.moveToNext()) {
val mappedContactBuilder = innerCursor.getLong(ContactsContract.CommonDataKinds.Phone.CONTACT_ID)
?.let { map[it] }
?: continue
innerCursor.getString(ContactsContract.CommonDataKinds.Phone.NUMBER)
?.let {
mappedContactBuilder.msisdns.add(
MappedMsisdn(
phoneNumber = it,
matrixId = null
)
)
}
}
}
}
// Get Emails
if (withEmails) {
contentResolver.query(
ContactsContract.CommonDataKinds.Email.CONTENT_URI,
arrayOf(
ContactsContract.CommonDataKinds.Email.CONTACT_ID,
ContactsContract.CommonDataKinds.Email.DATA
),
null,
null,
null)
?.use { innerCursor ->
while (innerCursor.moveToNext()) {
// This would allow you get several email addresses
// if the email addresses were stored in an array
val mappedContactBuilder = innerCursor.getLong(ContactsContract.CommonDataKinds.Email.CONTACT_ID)
?.let { map[it] }
?: continue
innerCursor.getString(ContactsContract.CommonDataKinds.Email.DATA)
?.let {
mappedContactBuilder.emails.add(
MappedEmail(
email = it,
matrixId = null
)
)
}
}
}
}
}.also { Timber.d("Took ${it}ms to fetch ${map.size} contact(s)") }
return map
.values
.filter { it.emails.isNotEmpty() || it.msisdns.isNotEmpty() }
.map { it.build() }
}
private fun Cursor.getString(column: String): String? {
return getColumnIndex(column)
.takeIf { it != -1 }
?.let { getString(it) }
}
private fun Cursor.getLong(column: String): Long? {
return getColumnIndex(column)
.takeIf { it != -1 }
?.let { getLong(it) }
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.core.contacts
import android.net.Uri
class MappedContactBuilder(
val id: Long,
val displayName: String
) {
var photoURI: Uri? = null
val msisdns = mutableListOf<MappedMsisdn>()
val emails = mutableListOf<MappedEmail>()
fun build(): MappedContact {
return MappedContact(
id = id,
displayName = displayName,
photoURI = photoURI,
msisdns = msisdns,
emails = emails
)
}
}
data class MappedContact(
val id: Long,
val displayName: String,
val photoURI: Uri? = null,
val msisdns: List<MappedMsisdn> = emptyList(),
val emails: List<MappedEmail> = emptyList()
)
data class MappedEmail(
val email: String,
val matrixId: String?
)
data class MappedMsisdn(
val phoneNumber: String,
val matrixId: String?
)

View File

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

View File

@ -23,6 +23,7 @@ import dagger.Binds
import dagger.Module import dagger.Module
import dagger.multibindings.IntoMap import dagger.multibindings.IntoMap
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment
import im.vector.riotx.features.contactsbook.ContactsBookFragment
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
import im.vector.riotx.features.crypto.quads.SharedSecuredStorageKeyFragment import im.vector.riotx.features.crypto.quads.SharedSecuredStorageKeyFragment
import im.vector.riotx.features.crypto.quads.SharedSecuredStoragePassphraseFragment import im.vector.riotx.features.crypto.quads.SharedSecuredStoragePassphraseFragment
@ -528,4 +529,9 @@ interface FragmentModule {
@IntoMap @IntoMap
@FragmentKey(WidgetFragment::class) @FragmentKey(WidgetFragment::class)
fun bindWidgetFragment(fragment: WidgetFragment): Fragment fun bindWidgetFragment(fragment: WidgetFragment): Fragment
@Binds
@IntoMap
@FragmentKey(ContactsBookFragment::class)
fun bindPhoneBookFragment(fragment: ContactsBookFragment): Fragment
} }

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.core.di
import android.content.Context
import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl
import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.glide.GlideImageLoader
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.ActiveSessionDataSource
import im.vector.riotx.core.glide.FactoryUrl
import java.io.InputStream
import javax.inject.Inject
/**
* This class is used to configure the library we use for images
*/
class ImageManager @Inject constructor(
private val context: Context,
private val activeSessionDataSource: ActiveSessionDataSource
) {
fun onSessionStarted(session: Session) {
// Do this call first
BigImageViewer.initialize(GlideImageLoader.with(context, session.getOkHttpClient()))
val glide = Glide.get(context)
// And this one. FIXME But are losing what BigImageViewer has done to add a Progress listener
glide.registry.replace(GlideUrl::class.java, InputStream::class.java, FactoryUrl(activeSessionDataSource))
}
}

View File

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

View File

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

View File

@ -20,6 +20,7 @@ package im.vector.riotx.core.epoxy.profiles
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
@ -36,16 +37,21 @@ abstract class ProfileMatrixItem : VectorEpoxyModel<ProfileMatrixItem.Holder>()
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem @EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var editable: Boolean = true
@EpoxyAttribute var userEncryptionTrustLevel: RoomEncryptionTrustLevel? = null @EpoxyAttribute var userEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
@EpoxyAttribute var clickListener: View.OnClickListener? = null @EpoxyAttribute var clickListener: View.OnClickListener? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
val bestName = matrixItem.getBestName() val bestName = matrixItem.getBestName()
val matrixId = matrixItem.id.takeIf { it != bestName } val matrixId = matrixItem.id
holder.view.setOnClickListener(clickListener) .takeIf { it != bestName }
// Special case for ThreePid fake matrix item
.takeIf { it != "@" }
holder.view.setOnClickListener(clickListener?.takeIf { editable })
holder.titleView.text = bestName holder.titleView.text = bestName
holder.subtitleView.setTextOrHide(matrixId) holder.subtitleView.setTextOrHide(matrixId)
holder.editableView.isVisible = editable
avatarRenderer.render(matrixItem, holder.avatarImageView) avatarRenderer.render(matrixItem, holder.avatarImageView)
holder.avatarDecorationImageView.setImageResource(userEncryptionTrustLevel.toImageRes()) holder.avatarDecorationImageView.setImageResource(userEncryptionTrustLevel.toImageRes())
} }
@ -55,5 +61,6 @@ abstract class ProfileMatrixItem : VectorEpoxyModel<ProfileMatrixItem.Holder>()
val subtitleView by bind<TextView>(R.id.matrixItemSubtitle) val subtitleView by bind<TextView>(R.id.matrixItemSubtitle)
val avatarImageView by bind<ImageView>(R.id.matrixItemAvatar) val avatarImageView by bind<ImageView>(R.id.matrixItemAvatar)
val avatarDecorationImageView by bind<ImageView>(R.id.matrixItemAvatarDecoration) val avatarDecorationImageView by bind<ImageView>(R.id.matrixItemAvatarDecoration)
val editableView by bind<View>(R.id.matrixItemEditable)
} }
} }

View File

@ -23,21 +23,21 @@ import androidx.fragment.app.FragmentTransaction
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
fun VectorBaseActivity.addFragment(frameId: Int, fragment: Fragment) { fun VectorBaseActivity.addFragment(frameId: Int, fragment: Fragment) {
supportFragmentManager.commitTransactionNow { add(frameId, fragment) } supportFragmentManager.commitTransaction { add(frameId, fragment) }
} }
fun <T : Fragment> VectorBaseActivity.addFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) { fun <T : Fragment> VectorBaseActivity.addFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) {
supportFragmentManager.commitTransactionNow { supportFragmentManager.commitTransaction {
add(frameId, fragmentClass, params.toMvRxBundle(), tag) add(frameId, fragmentClass, params.toMvRxBundle(), tag)
} }
} }
fun VectorBaseActivity.replaceFragment(frameId: Int, fragment: Fragment, tag: String? = null) { fun VectorBaseActivity.replaceFragment(frameId: Int, fragment: Fragment, tag: String? = null) {
supportFragmentManager.commitTransactionNow { replace(frameId, fragment, tag) } supportFragmentManager.commitTransaction { replace(frameId, fragment, tag) }
} }
fun <T : Fragment> VectorBaseActivity.replaceFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) { fun <T : Fragment> VectorBaseActivity.replaceFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) {
supportFragmentManager.commitTransactionNow { supportFragmentManager.commitTransaction {
replace(frameId, fragmentClass, params.toMvRxBundle(), tag) replace(frameId, fragmentClass, params.toMvRxBundle(), tag)
} }
} }

View File

@ -19,6 +19,9 @@ package im.vector.riotx.core.extensions
import android.os.Bundle import android.os.Bundle
import android.util.Patterns import android.util.Patterns
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
import im.vector.matrix.android.api.extensions.ensurePrefix
fun Boolean.toOnOff() = if (this) "ON" else "OFF" fun Boolean.toOnOff() = if (this) "ON" else "OFF"
@ -33,3 +36,15 @@ fun <T : Fragment> T.withArgs(block: Bundle.() -> Unit) = apply { arguments = Bu
* Check if a CharSequence is an email * Check if a CharSequence is an email
*/ */
fun CharSequence.isEmail() = Patterns.EMAIL_ADDRESS.matcher(this).matches() fun CharSequence.isEmail() = Patterns.EMAIL_ADDRESS.matcher(this).matches()
/**
* Check if a CharSequence is a phone number
*/
fun CharSequence.isMsisdn(): Boolean {
return try {
PhoneNumberUtil.getInstance().parse(ensurePrefix("+"), null)
true
} catch (e: NumberParseException) {
false
}
}

View File

@ -16,26 +16,32 @@
package im.vector.riotx.core.extensions package im.vector.riotx.core.extensions
import android.app.Activity
import android.os.Parcelable import android.os.Parcelable
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.selectTxtFileToWrite
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
fun VectorBaseFragment.addFragment(frameId: Int, fragment: Fragment) { fun VectorBaseFragment.addFragment(frameId: Int, fragment: Fragment) {
parentFragmentManager.commitTransactionNow { add(frameId, fragment) } parentFragmentManager.commitTransaction { add(frameId, fragment) }
} }
fun <T : Fragment> VectorBaseFragment.addFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) { fun <T : Fragment> VectorBaseFragment.addFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) {
parentFragmentManager.commitTransactionNow { parentFragmentManager.commitTransaction {
add(frameId, fragmentClass, params.toMvRxBundle(), tag) add(frameId, fragmentClass, params.toMvRxBundle(), tag)
} }
} }
fun VectorBaseFragment.replaceFragment(frameId: Int, fragment: Fragment) { fun VectorBaseFragment.replaceFragment(frameId: Int, fragment: Fragment) {
parentFragmentManager.commitTransactionNow { replace(frameId, fragment) } parentFragmentManager.commitTransaction { replace(frameId, fragment) }
} }
fun <T : Fragment> VectorBaseFragment.replaceFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) { fun <T : Fragment> VectorBaseFragment.replaceFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) {
parentFragmentManager.commitTransactionNow { parentFragmentManager.commitTransaction {
replace(frameId, fragmentClass, params.toMvRxBundle(), tag) replace(frameId, fragmentClass, params.toMvRxBundle(), tag)
} }
} }
@ -51,21 +57,21 @@ fun <T : Fragment> VectorBaseFragment.addFragmentToBackstack(frameId: Int, fragm
} }
fun VectorBaseFragment.addChildFragment(frameId: Int, fragment: Fragment, tag: String? = null) { fun VectorBaseFragment.addChildFragment(frameId: Int, fragment: Fragment, tag: String? = null) {
childFragmentManager.commitTransactionNow { add(frameId, fragment, tag) } childFragmentManager.commitTransaction { add(frameId, fragment, tag) }
} }
fun <T : Fragment> VectorBaseFragment.addChildFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) { fun <T : Fragment> VectorBaseFragment.addChildFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) {
childFragmentManager.commitTransactionNow { childFragmentManager.commitTransaction {
add(frameId, fragmentClass, params.toMvRxBundle(), tag) add(frameId, fragmentClass, params.toMvRxBundle(), tag)
} }
} }
fun VectorBaseFragment.replaceChildFragment(frameId: Int, fragment: Fragment, tag: String? = null) { fun VectorBaseFragment.replaceChildFragment(frameId: Int, fragment: Fragment, tag: String? = null) {
childFragmentManager.commitTransactionNow { replace(frameId, fragment, tag) } childFragmentManager.commitTransaction { replace(frameId, fragment, tag) }
} }
fun <T : Fragment> VectorBaseFragment.replaceChildFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) { fun <T : Fragment> VectorBaseFragment.replaceChildFragment(frameId: Int, fragmentClass: Class<T>, params: Parcelable? = null, tag: String? = null) {
childFragmentManager.commitTransactionNow { childFragmentManager.commitTransaction {
replace(frameId, fragmentClass, params.toMvRxBundle(), tag) replace(frameId, fragmentClass, params.toMvRxBundle(), tag)
} }
} }
@ -89,3 +95,27 @@ fun Fragment.getAllChildFragments(): List<Fragment> {
// Define a missing constant // Define a missing constant
const val POP_BACK_STACK_EXCLUSIVE = 0 const val POP_BACK_STACK_EXCLUSIVE = 0
fun Fragment.queryExportKeys(userId: String, requestCode: Int) {
val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
selectTxtFileToWrite(
activity = requireActivity(),
fragment = this,
defaultFileName = "element-megolm-export-$userId-$timestamp.txt",
chooserHint = getString(R.string.keys_backup_setup_step1_manual_export),
requestCode = requestCode
)
}
fun Activity.queryExportKeys(userId: String, requestCode: Int) {
val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
selectTxtFileToWrite(
activity = this,
fragment = null,
defaultFileName = "element-megolm-export-$userId-$timestamp.txt",
chooserHint = getString(R.string.keys_backup_setup_step1_manual_export),
requestCode = requestCode
)
}

View File

@ -38,13 +38,13 @@ inline fun <T, R : Comparable<R>> Iterable<T>.lastMinBy(selector: (T) -> R): T?
/** /**
* Call each for each item, and between between each items * Call each for each item, and between between each items
*/ */
inline fun <T> Collection<T>.join(each: (T) -> Unit, between: (T) -> Unit) { inline fun <T> Collection<T>.join(each: (Int, T) -> Unit, between: (Int, T) -> Unit) {
val lastIndex = size - 1 val lastIndex = size - 1
forEachIndexed { idx, t -> forEachIndexed { idx, t ->
each(t) each(idx, t)
if (idx != lastIndex) { if (idx != lastIndex) {
between(t) between(idx, t)
} }
} }
} }

View File

@ -24,20 +24,14 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.riotx.core.services.VectorSyncService import im.vector.riotx.core.services.VectorSyncService
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.session.SessionListener
import timber.log.Timber import timber.log.Timber
fun Session.configureAndStart(context: Context, fun Session.configureAndStart(context: Context) {
pushRuleTriggerListener: PushRuleTriggerListener, Timber.i("Configure and start session for $myUserId")
sessionListener: SessionListener) {
open() open()
addListener(sessionListener)
setFilter(FilterService.FilterPreset.RiotFilter) setFilter(FilterService.FilterPreset.RiotFilter)
Timber.i("Configure and start session for ${this.myUserId}")
startSyncing(context) startSyncing(context)
refreshPushers() refreshPushers()
pushRuleTriggerListener.startWithSession(this)
} }
fun Session.startSyncing(context: Context) { fun Session.startSyncing(context: Context) {
@ -65,3 +59,12 @@ fun Session.hasUnsavedKeys(): Boolean {
return cryptoService().inboundGroupSessionsCount(false) > 0 return cryptoService().inboundGroupSessionsCount(false) > 0
&& cryptoService().keysBackupService().state != KeysBackupState.ReadyToBackUp && cryptoService().keysBackupService().state != KeysBackupState.ReadyToBackUp
} }
fun Session.cannotLogoutSafely(): Boolean {
// has some encrypted chat
return hasUnsavedKeys()
// has local cross signing keys
|| (cryptoService().crossSigningService().allPrivateKeysKnown()
// That are not backed up
&& !sharedSecretStorageService.isRecoverySetup())
}

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