Merge remote-tracking branch 'upstream/master' into sc

Conflicts:
	vector/build.gradle
	vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
	vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
	vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt
	vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListDisplayModeFilter.kt
	vector/src/main/res/drawable/bg_login_server.xml
	vector/src/main/res/drawable/bg_login_server_checked.xml
	vector/src/main/res/layout/fragment_login_server_selection.xml
This commit is contained in:
SpiritCroc 2020-07-17 13:39:52 +02:00
commit 95850440bd
1066 changed files with 27987 additions and 8359 deletions

View File

@ -7,6 +7,7 @@
<w>ciphertext</w>
<w>coroutine</w>
<w>decryptor</w>
<w>displayname</w>
<w>emoji</w>
<w>emojis</w>
<w>fdroid</w>

View File

@ -1,3 +1,118 @@
Changes in Element 1.0.0 (2020-07-15)
===================================================
Features ✨:
- Re-branding: The app is now called Element. New name, new themes, new icons, etc. More details here: https://element.io/blog/welcome-to-element/ (#1691)
Bugfix 🐛:
- Video calls are shown as a voice ones in the timeline (#1676)
- Fix regression: not able to create a room without IS configured (#1679)
Changes in Riot.imX 0.91.5 (2020-07-11)
===================================================
Features ✨:
- 3pid invite: it is now possible to invite people by email. An Identity Server has to be configured (#548)
Improvements 🙌:
- Cleaning chunks with lots of events as long as a threshold has been exceeded (35_000 events in DB) (#1634)
- Creating and listening to EventInsertEntity. (#1634)
- Handling (almost) properly the groups fetching (#1634)
- Improve fullscreen media display (#327)
- Setup server recovery banner (#1648)
- Set up SSSS from security settings (#1567)
- New lab setting to add 'unread notifications' tab to main screen
- Render third party invite event (#548)
- Display three pid invites in the room members list (#548)
Bugfix 🐛:
- Integration Manager: Wrong URL to review terms if URL in config contains path (#1606)
- Regression Composer does not grow, crops out text (#1650)
- Bug / Unwanted draft (#698)
- All users seems to be able to see the enable encryption option in room settings (#1341)
- Leave room only leaves the current version (#1656)
- Regression | Share action menu do not work (#1647)
- verification issues on transition (#1555)
- Fix issue when restoring keys backup using recovery key
SDK API changes ⚠️:
- CreateRoomParams has been updated
Build 🧱:
- Upgrade some dependencies
- Revert to build-tools 3.5.3
Other changes:
- Use Intent.ACTION_CREATE_DOCUMENT to save megolm key or recovery key in a txt file
- Use `Context#withStyledAttributes` extension function (#1546)
Changes in Riot.imX 0.91.4 (2020-07-06)
===================================================
Features ✨:
- Re-activate Wellknown support with updated UI (#1614)
Improvements 🙌:
- Upload device keys only once to the homeserver and fix crash when no network (#1629)
Bugfix 🐛:
- Fix crash when coming from a notification (#1601)
- Fix Exception when importing keys (#1576)
- File isn't downloaded when another file with the same name already exists (#1578)
- saved images don't show up in gallery (#1324)
- Fix reply fallback leaking sender locale (#429)
Build 🧱:
- Fix lint false-positive about WorkManager (#1012)
- Upgrade build-tools from 3.5.3 to 3.6.3
- Upgrade gradle from 5.4.1 to 5.6.4
Changes in Riot.imX 0.91.3 (2020-07-01)
===================================================
Notes:
- This version is the third beta version of RiotX codebase published as Riot-Android on the PlayStore.
- Changelog below includes changes of v0.91.0, v0.91.1, and v0.91.2, because the first beta versions have been tagged and
published from the branch feature/migration_from_legacy.
- This version uses temporary name `Riot.imX`, to distinguish the app with RiotX app.
Features ✨:
- Call with WebRTC support (##611)
- Add capability to change the display name (#1529)
Improvements 🙌:
- "Add Matrix app" menu is now always visible (#1495)
- Handle `/op`, `/deop`, and `/nick` commands (#12)
- Prioritising Recovery key over Recovery passphrase (#1463)
- Room Settings: Name, Topic, Photo, Aliases, History Visibility (#1455)
- Update user avatar (#1054)
- Allow self-signed certificate (#1564)
- Improve file download and open in timeline
- Catchup tab is removed temporarily (#1565)
- Render room avatar change (#1319)
Bugfix 🐛:
- Fix dark theme issue on login screen (#1097)
- Incomplete predicate in RealmCryptoStore#getOutgoingRoomKeyRequest (#1519)
- User could not redact message that they have sent (#1543)
- Use vendor prefix for non merged MSC (#1537)
- Compress images before sending (#1333)
- Searching by displayname is case sensitive (#1468)
- Fix layout overlap issue (#1407)
Build 🧱:
- Enable code optimization (Proguard)
- SDK is now API level 21 minimum, and so RiotX (#405)
Other changes:
- Use `SharedPreferences#edit` extension function consistently (#1545)
- Use `retrofit2.Call.awaitResponse` extension provided by Retrofit 2. (#1526)
- Fix minor typo in contribution guide (#1512)
- Fix self-assignment of callback in `DefaultRoomPushRuleService#setRoomNotificationState` (#1520)
- Random housekeeping clean-ups indicated by Lint (#1520, #1541)
- Keys Backup API now use the unstable prefix (#1503)
- Remove deviceId from /keys/upload/{deviceId} as not spec-compliant (#1502)
Changes in RiotX 0.22.0 (2020-06-15)
===================================================
@ -591,7 +706,7 @@ Mode details here: https://medium.com/@RiotChat/introducing-the-riotx-beta-for-a
=======================================================
Changes in RiotX 0.X.0 (2020-XX-XX)
Changes in Element 1.X.X (2020-XX-XX)
===================================================
Features ✨:

View File

@ -19,7 +19,11 @@ An Android Studio template has been added to the project to help creating all fi
To install the template (to be done only once):
- Go to folder `./tools/template`.
- Run the script `./configure.sh`.
- Mac OSX: Run the script `./configure.sh`.
Linux: Run `ANDROID_STUDIO=/path/to/android-studio ./configure`
- e.g. `ANDROID_STUDIO=/usr/local/android-studio ./configure`
- Restart Android Studio.
To create a new screen:
@ -27,7 +31,7 @@ To create a new screen:
- Then right click on the package, and select `New/New Vector/RiotX Feature`.
- Follow the Wizard, especially replace `Main` by something more relevant to your feature.
- Click on `Finish`.
- Remainning steps are described as TODO in the generated files, or will be pointed out by the compilator, or at runtime :)
- Remaining steps are described as TODO in the generated files, or will be pointed out by the compilator, or at runtime :)
Note that if the templates are modified, the only things to do is to restart Android Studio for the change to take effect.

View File

@ -11,8 +11,8 @@ RiotX is an Android Matrix Client currently in beta but in active development.
It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-android) with a new user experience. RiotX will become the official replacement as soon as all features are implemented.
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.riotx)
[<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="60">](https://f-droid.org/app/im.vector.riotx)
[<img src="resources/img/google-play-badge.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.riotx)
[<img src="resources/img/f-droid-badge.png" alt="Get it on F-Droid" height="60">](https://f-droid.org/app/im.vector.riotx)
Nightly build: [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)

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

@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.3.50'
ext.kotlin_version = '1.3.72'
repositories {
google()
jcenter()
@ -10,12 +10,13 @@ buildscript {
}
}
dependencies {
// Warning: 3.6.3 leads to infinite gradle builds. Stick to 3.5.3 for the moment
classpath 'com.android.tools.build:gradle:3.5.3'
classpath 'com.google.gms:google-services:4.3.2'
classpath "com.airbnb.okreplay:gradle-plugin:1.5.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1'
classpath 'com.google.android.gms:oss-licenses-plugin:0.9.5'
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@ -38,6 +39,8 @@ allprojects {
includeGroupByRegex "com\\.github\\.yalantis"
// JsonViewer
includeGroupByRegex 'com\\.github\\.BillCarsonFr'
// PhotoView
includeGroupByRegex 'com\\.github\\.chrisbanes'
}
}
maven {

425
docs/voip_signaling.md Normal file
View File

@ -0,0 +1,425 @@
Useful links:
- https://codelabs.developers.google.com/codelabs/webrtc-web/#0
- http://webrtc.github.io/webrtc-org/native-code/android/
╔════════════════════════════════════════════════╗
║ ║
║A] Placing a call offer ║
║ ║
╚════════════════════════════════════════════════╝
┌───────────────┐
│ Matrix │
├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
┌─────────────────┐ │ ┌───────────────────────────┐ ┌─────────────────┐
│ Caller │ │ Signaling Room │ │ │ Callee │
└─────────────────┘ │ ├───────────────────────────┤ └─────────────────┘
┌────┐ │ │ │
│ 3 │ │ │ ┌────────────────────┐ │
┌─────────────────┐──────┴────┴──────────────────────────┼─▶│ m.call.invite │ │ │ ┌─────────────────┐
│ │ │ │ │ mx event │ │ │ │
│ │ │ └────────────────────┘ │ │ │ │
│ │ │ │ │ │ │
│ Riot.im │ │ │ │ │ Riot.im │
┌──│ App │ │ │ │ │ App │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ └─────────────────┘ │ │ │ └─────────────────┘
┌────┤ ▲ │ │ │
│ 1 │ ├────┐ │ └───────────────────────────┘
└────┤ │ 2 │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
│ ┌──┴────┴─────────┐ ┌─────────────────┐
│ │ │ │ │
│ │ │ │ │
│ │ WebRtc │ │ WebRtc │
└─▶│ │ │ │
│ │ │ │
│ │ │ │
└─────────────────┘ └─────────────────┘
┌────┐
│ 1 │ The Caller app get access to system resources (camera, mic), eventually stun/turn servers, define some
└────┘ constrains (video quality, format) and pass it to WebRtc in order to create a Peer Call offer
┌────┐
│ 2 │ The WebRtc layer creates a call Offer (sdp) that needs to be sent to callee
└────┘
┌────┐ The app layer, takes the webrtc offer, encapsulate it in a matrix event adds a callId and send it to the other
│ 3 │ user via the room
└────┘
┌──────────────┐
│ mx event │
├──────────────┴────────┐
│ type: m.call.invite │
│ + callId │
│ │
│ ┌──────────────────┐ │
│ │ webrtc sdp │ │
│ └──────────────────┘ │
└───────────────────────┘
╔════════════════════════════════════════════════╗
║ ║
║B] Sending connection establishment info ║
║ ║
╚════════════════════════════════════════════════╝
┌───────────────┐
│ Matrix │
├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
┌─────────────────┐ │ ┌───────────────────────────┐ ┌─────────────────┐
│ Caller │ │ Signaling Room │ │ │ Callee │
└─────────────────┘ │ ├───────────────────────────┤ └─────────────────┘
│ ┌────────────────────┐ │ │
│ │ │ m.call.invite │ │
┌─────────────────┐ │ │ mx event │ │ │ ┌─────────────────┐
│ │ ┌────┐ │ │ └────────────────────┘ │ │ │
│ │ │ 3 │ │ ┌────────────────────┐ │ │ │ │
│ │──────┴────┴───────┼──────────────────┼─▶│ m.call.candidates │ │ │ │
│ Riot.im │ │ │ mx event │ │ │ │ Riot.im │
│ App │ │ │ └────────────────────┘ │ │ App │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
└─────────────────┘ │ │ │ └─────────────────┘
▲ │ │ │
├────┐ │ └───────────────────────────┘
│ 2 │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
┌───────┴────┴────┐ ┌─────────────────┐
│ │ │ │
│ │ │ │
│ WebRtc │ ┌───────────────┐ │ WebRtc │
│ │ │ Stun / Turn │ │ │
│ │ ├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │
│ │ │ │ │
└─────────────────┘ │ └─────────────────┘
▲ │
│ │
└──────────┬────┬───────────▶ │
┌───────────────┐ │ 1 │ │
│ │ └────┘ │
│ Network Stack │ │
│ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
│ │
└───────────────┘
┌────┐
│ 1 │ The WebRtc layer gathers information on how it can be reach by the other peer directly (Ice candidates)
└────┘
┌──────────────────────────────────────────────────────────────────┐
│candidate:1 1 tcp 1518149375 127.0.0.1 35990 typ host │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│candidate:2 1 UDP 2130706431 192.168.1.102 1816 typ host │
└──────────────────────────────────────────────────────────────────┘
┌────┐
│ 2 │ The WebRTC layer notifies the App layer when it finds new Ice Candidates
└────┘
┌────┐ The app layer, takes the ice candidates, encapsulate them in one or several matrix event adds the callId and
│ 3 │ send it to the other user via the room
└────┘
┌──────────────┐
│ mx event │
├──────────────┴────────────────────────┐
│ type: m.call.candidates │
│ │
│ +CallId │
│ │
│ ┌──────────────────┐ │
│ │ice candidate sdp │ │
│ └──────────────────┘ │
│ ┌──────────────────┐ │
│ │ice candidate sdp │ │
│ └──────────────────┘ │
│ ┌──────────────────┐ │
│ │ice candidate sdp │ │
│ └──────────────────┘ │
└───────────────────────────────────────┘
╔════════════════════════════════════════════════╗
║ ║
║C] Receiving a call offer ║
║ ║
╚════════════════════════════════════════════════╝
┌───────────────┐
│ Matrix │
├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
│ ┌─────────────────┐
│ │ Callee │
┌─────────────────┐ │ ┌───────────────────────────┐ └─────────────────┘
│ Caller │ │ Signaling Room │ │
└─────────────────┘ │ ├───────────────────────────┤
│ ┌────────────────────┐ │ │ ┌─────────────────┐
│ │ │ m.call.invite │───┼────────────────────────────┬────┬───▶│ │
┌─────────────────┐ │ │ mx event │ │ │ │ 1 │ │ │
│ │ │ │ └────────────────────┘ │ └────┘ │ │
│ │ │ ┌────────────────────┐ │ │ │ Riot.im │
│ │ │ │ │ m.call.candidates │ │ │ App │
│ Riot.im │ │ │ mx event │ │ │ │ │
│ App │ │ │ └────────────────────┘ │ │ │
│ │ │ ┌────────────────────┐◀──┼─────────────────┼───┬────┬───────────┤ │
│ │◀──────────────────┼──────────────────┼──│ m.call.answer │ │ │ 4 │ └──┬──────────────┘
│ │ │ │ mx event │ │ │ └────┘ ├────┐ ▲
└────┬────────────┘ │ │ └────────────────────┘ │ │ 2 │ ├────┐
│ │ │ │ ├────┘ │ 3 │
│ │ └───────────────────────────┘ ┌──▼─────────┴────┤
┌────▼────────────┐ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │
│ │ │ │
│ │ │ WebRtc │
│ WebRtc │ │ ┌──┴─────────────────┐
│ │ │ │ caller offer │
┌──────────┴─────────┐ │ │ └──┬─────────────────┘
│ callee answer │ │ └─────────────────┘
└────────────────────┴───────┘
┌────┐
│ 1 │ Bob receives a call.invite event in a room, then creates a WebRTC peer connection object
└────┘
┌────┐
│ 2 │ The encapsulated call offer sdp from the mx event is transmitted to WebRTC
└────┘
┌────┐
│ 3 │ WebRTC then creates a call answer for the offer and send it back to app layer
└────┘
┌────┐ The app layer, takes the webrtc answer, encapsulate it in a matrix event adds a callId and send it to the
│ 3 │ other user via the room
└────┘
┌──────────────┐
│ mx event │
├──────────────┴────────┐
│ type: m.call.answer │
│ + callId │
│ │
│ ┌──────────────────┐ │
│ │ webrtc sdp │ │
│ └──────────────────┘ │
└───────────────────────┘
╔════════════════════════════════════════════════╗
║ ║
║D] Callee sends connection establishment info ║
║ ║
╚════════════════════════════════════════════════╝
┌───────────────┐
│ Matrix │
├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
┌─────────────────┐ │ ┌───────────────────────────┐ ┌─────────────────┐
│ Caller │ │ Signaling Room │ │ │ Callee │
└─────────────────┘ │ ├───────────────────────────┤ └─────────────────┘
│ ┌────────────────────┐ │ │
│ │ │ m.call.invite │ │
┌─────────────────┐ │ │ mx event │ │ │ ┌─────────────────┐
│ │ │ │ └────────────────────┘ │ │ │
│ │ │ ┌────────────────────┐ │ │ │ │
│ │ │ │ │ m.call.candidates │ │ │ │
│ Riot.im │ │ │ mx event │ │ │ │ Riot.im │
│ App │ │ │ └────────────────────┘ │ ┌────┐ │ App │
│ │ │ ┌────────────────────┐ │ │ │ 3 │ │ │
│ │◀──────────────────┼┐ │ │ m.call.answer │ │ ┌───────┴────┴────────│ │
│ │ │ │ │ mx event │ │ ││ │ │
└─────────────────┘ ││ │ └────────────────────┘ │ │ └─────────────────┘
│ │ │ ┌────────────────────┐ │ ││ ▲
│ │└─────────────────┼──│ m.call.candidates │ │ │ ├────┐
▼ │ │ mx event │◀──┼────────────────┘│ │ 2 │
┌─────────────────┐ │ │ └────────────────────┘ │ ┌────┴────┴───────┐
│ │ └───────────────────────────┘ │ │ │
│ │ │ │ │
│ WebRtc │ │ │ WebRtc │
│ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ┌───┴────────────────┐
│ │ │ │ caller offer │
┌────────┴───────────┐ │ │ └───┬────────────────┘
│ callee answer ├─────┘ ┌───────────────┐ └─────────────────┘
├────────────────────┤ │ Stun / Turn │ ▲
│ callee ice │ ├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ┌────┐ │
│ candidates │ │ │ 1 │ │
└────────────────────┘ │ ├────┴──┴───────┐
│ │ │
│ │ Network Stack │
│◀─────────────────────┤ │
│ │ │
│ └───────────────┘
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
┌────┐
│ 1 │ The WebRtc layer gathers information on how it can be reach by the other peer directly (Ice candidates)
└────┘
┌──────────────────────────────────────────────────────────────────┐
│candidate:1 1 tcp 1518149375 127.0.0.1 35990 typ host │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│candidate:2 1 UDP 2130706431 192.168.1.102 1816 typ host │
└──────────────────────────────────────────────────────────────────┘
┌────┐
│ 2 │ The WebRTC layer notifies the App layer when it finds new Ice Candidates
└────┘
┌────┐ The app layer, takes the ice candidates, encapsulate them in one or several matrix event adds the callId and
│ 3 │ send it to the other user via the room
└────┘
┌──────────────┐
│ mx event │
├──────────────┴────────────────────────┐
│ type: m.call.candidates │
│ │
│ +CallId │
│ │
│ ┌──────────────────┐ │
│ │ice candidate sdp │ │
│ └──────────────────┘ │
│ ┌──────────────────┐ │
│ │ice candidate sdp │ │
│ └──────────────────┘ │
│ ┌──────────────────┐ │
│ │ice candidate sdp │ │
│ └──────────────────┘ │
└───────────────────────────────────────┘
╔════════════════════════════════════════════════╗
║ ║
║D] Caller Callee connection ║
║ ║
╚════════════════════════════════════════════════╝
┌───────────────┐
┌─────────────────┐ │ Matrix │ ┌─────────────────┐
│ Caller │ ├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ Callee │
└─────────────────┘ │ └─────────────────┘
┌─────────────────┐ │ ┌─────────────────┐
│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
│ Riot.im │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ Riot.im │
│ App │ │ App │
│ │ │ │
│ │ │ │
│ │ │ │
└─────────────────┘ └─────────────────┘
┌───────────────┐
│ Internet │
├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
┌─────────────────┐ │ ┌─────────────────┐
│ │ │ │ │
│ ├───────────────────────────────────────────────────────────────────────────────────┴─────────────────────┤ │
│ WebRtc │█████████████████████████████████████████████████████████████████████████████████████████████████████████│ WebRtc │
┌─────────────┴──────┐ ├────────────────────────────────────────┬──────────────────────────┬───────────────┬─────────────────────┤ ┌─────┴──────────────┐
│ callee answer │ │ │ │ Video / Audio Stream │ │ │ caller offer │
├────────────────────┤ │ └──────────────────────────┘ │ │ ├────────────────────┤
│ callee ice ├──────────┘ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ └───────────┤ caller ice │
│ candidates │ │ candidates │
└────────────────────┘ └────────────────────┘
┌─────────────────────────────────────────────────────┐
│ │░
│ If connection is impossible (firewall), and a turn │░
│server is available, connection could happen through │░
│ a relay │░
│ │░
└─────────────────────────────────────────────────────┘░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
┌───────────────┐
│ Internet │
└─┬─────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
┌─────────────────┐ │ ┌─────────────────┐
│ │ │ ┌─────────────────────────┐ │ │
│ ├───────────────────────────────────────┐│ │ │ │ │
│ WebRtc │███████████████████████████████████████││ │ │ WebRtc │
│ ├───────────────────────────────────────┘│ │ │ │ │
│ │ ┌────────┴─────────────────┐ │ Relay │┌─────────────────────────────────────┤ │
┌───────────────┴────┐ │ │ Video / Audio Stream │ │ ││█████████████████████████████████████│ ┌───────┴────────────┐
│ callee answer ├────────────┘ └────────┬─────────────────┘ │ │└─────────────────────────────────────┴─────────┤ caller offer │
├────────────────────┤ │ │ │ ├────────────────────┤
│ callee ice │ │ │ │ │ caller ice │
│ candidates │ └─────────────────────────┘ │ │ candidates │
└────────────────────┘ │ └────────────────────┘
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─

View File

@ -8,7 +8,7 @@
# The setting is particularly useful for tweaking memory settings.
android.enableJetifier=true
android.useAndroidX=true
org.gradle.jvmargs=-Xmx8192m
org.gradle.jvmargs=-Xmx2048m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects

View File

@ -1,6 +1,6 @@
#Fri Sep 27 10:10:35 CEST 2019
#Thu Jul 02 12:33:07 CEST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

View File

@ -6,7 +6,7 @@ android {
compileSdkVersion 29
defaultConfig {
minSdkVersion 16
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
@ -39,7 +39,7 @@ dependencies {
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
// Paging
implementation "androidx.paging:paging-runtime-ktx:2.1.0"
implementation "androidx.paging:paging-runtime-ktx:2.1.2"
// Logging
implementation 'com.jakewharton.timber:timber:4.7.1'

View File

@ -16,12 +16,15 @@
package im.vector.matrix.rx
import android.net.Uri
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.notification.RoomNotificationState
@ -69,6 +72,13 @@ class RxRoom(private val room: Room) {
}
}
fun liveStateEvents(eventTypes: Set<String>): Observable<List<Event>> {
return room.getStateEventsLive(eventTypes).asObservable()
.startWithCallable {
room.getStateEvents(eventTypes)
}
}
fun liveReadMarker(): Observable<Optional<String>> {
return room.getReadMarkerLive().asObservable()
}
@ -101,6 +111,34 @@ class RxRoom(private val room: Room) {
fun invite(userId: String, reason: String? = null): Completable = completableBuilder<Unit> {
room.invite(userId, reason, it)
}
fun invite3pid(threePid: ThreePid): Completable = completableBuilder<Unit> {
room.invite3pid(threePid, it)
}
fun updateTopic(topic: String): Completable = completableBuilder<Unit> {
room.updateTopic(topic, it)
}
fun updateName(name: String): Completable = completableBuilder<Unit> {
room.updateName(name, it)
}
fun addRoomAlias(alias: String): Completable = completableBuilder<Unit> {
room.addRoomAlias(alias, it)
}
fun updateCanonicalAlias(alias: String): Completable = completableBuilder<Unit> {
room.updateCanonicalAlias(alias, it)
}
fun updateHistoryReadability(readability: RoomHistoryVisibility): Completable = completableBuilder<Unit> {
room.updateHistoryReadability(readability, it)
}
fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder<Unit> {
room.updateAvatar(avatarUri, fileName, it)
}
}
fun Room.rx(): RxRoom {

View File

@ -17,14 +17,20 @@
package im.vector.matrix.rx
import androidx.paging.PagedList
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.api.session.pushers.Pusher
import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.sync.SyncState
@ -36,9 +42,11 @@ import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.functions.Function3
class RxSession(private val session: Session) {
@ -165,6 +173,42 @@ class RxSession(private val session: Session) {
session.widgetService().getRoomWidgets(roomId, widgetId, widgetTypes, excludedTypes)
}
}
fun liveRoomChangeMembershipState(): Observable<Map<String, ChangeMembershipState>> {
return session.getChangeMembershipsLive().asObservable()
}
fun liveSecretSynchronisationInfo(): Observable<SecretsSynchronisationInfo> {
return Observable.combineLatest<List<UserAccountData>, Optional<MXCrossSigningInfo>, Optional<PrivateKeysInfo>, SecretsSynchronisationInfo>(
liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME)),
liveCrossSigningInfo(session.myUserId),
liveCrossSigningPrivateKeys(),
Function3 { _, crossSigningInfo, pInfo ->
// first check if 4S is already setup
val is4SSetup = session.sharedSecretStorageService.isRecoverySetup()
val isCrossSigningEnabled = crossSigningInfo.getOrNull() != null
val isCrossSigningTrusted = crossSigningInfo.getOrNull()?.isTrusted() == true
val allPrivateKeysKnown = pInfo.getOrNull()?.allKnown().orFalse()
val keysBackupService = session.cryptoService().keysBackupService()
val currentBackupVersion = keysBackupService.currentBackupVersion
val megolmBackupAvailable = currentBackupVersion != null
val savedBackupKey = keysBackupService.getKeyBackupRecoveryKeyInfo()
val megolmKeyKnown = savedBackupKey?.version == currentBackupVersion
SecretsSynchronisationInfo(
isBackupSetup = is4SSetup,
isCrossSigningEnabled = isCrossSigningEnabled,
isCrossSigningTrusted = isCrossSigningTrusted,
allPrivateKeysKnown = allPrivateKeysKnown,
megolmBackupAvailable = megolmBackupAvailable,
megolmSecretKnown = megolmKeyKnown,
isMegolmKeyIn4S = session.sharedSecretStorageService.isMegolmKeyInBackup()
)
}
)
.distinctUntilChanged()
}
}
fun Session.rx(): RxSession {

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

@ -23,7 +23,7 @@ android {
testOptions.unitTests.includeAndroidResources = true
defaultConfig {
minSdkVersion 16
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "0.0.1"
@ -31,14 +31,26 @@ android {
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// The following argument makes the Android Test Orchestrator run its
// "pm clear" command after each test invocation. This command ensures
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\""
resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\""
defaultConfig {
consumerProguardFiles 'proguard-rules.pro'
}
}
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
}
buildTypes {
debug {
// Set to true to log privacy or sensible data, such as token
buildConfigField "boolean", "LOG_PRIVATE_DATA", project.property("vector.debugPrivateData")
@ -49,9 +61,6 @@ android {
release {
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false"
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE"
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
@ -113,6 +122,7 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.core:core-ktx:1.3.0"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
@ -161,6 +171,10 @@ dependencies {
// Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23'
// Web RTC
// TODO meant for development purposes only. See http://webrtc.github.io/webrtc-org/native-code/android/
implementation 'org.webrtc:google-webrtc:1.0.+'
debugImplementation 'com.airbnb.okreplay:okreplay:1.5.0'
releaseImplementation 'com.airbnb.okreplay:noop:1.5.0'
androidTestImplementation 'com.airbnb.okreplay:espresso:1.5.0'
@ -175,6 +189,7 @@ dependencies {
// Plant Timber tree for test
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
kaptAndroidTest "com.google.dagger:dagger-compiler:$daggerVersion"
androidTestImplementation 'androidx.test:core:1.2.0'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
@ -187,4 +202,6 @@ dependencies {
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
// Plant Timber tree for test
androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
androidTestUtil 'androidx.test:orchestrator:1.2.0'
}

View File

@ -19,3 +19,64 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
### EVENT BUS ###
-keepattributes *Annotation*
-keepclassmembers class * {
@org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
### MOSHI ###
# JSR 305 annotations are for embedding nullability information.
-dontwarn javax.annotation.**
-keepclasseswithmembers class * {
@com.squareup.moshi.* <methods>;
}
-keep @com.squareup.moshi.JsonQualifier interface *
# Enum field names are used by the integrated EnumJsonAdapter.
# values() is synthesized by the Kotlin compiler and is used by EnumJsonAdapter indirectly
# Annotate enums with @JsonClass(generateAdapter = false) to use them with Moshi.
-keepclassmembers @com.squareup.moshi.JsonClass class * extends java.lang.Enum {
<fields>;
**[] values();
}
-keep class kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoaderImpl
-keepclassmembers class kotlin.Metadata {
public <methods>;
}
### OKHTTP for Android Studio ###
-keep class okhttp3.Headers { *; }
-keep interface okhttp3.Interceptor.* { *; }
### OLM JNI ###
-keep class org.matrix.olm.** { *; }
### Webrtc
-keep class org.webrtc.** { *; }
### Serializable persisted classes
# https://www.guardsquare.com/en/products/proguard/manual/examples#serializable
-keepnames class * implements java.io.Serializable
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
!static !transient <fields>;
!private <fields>;
!private <methods>;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}

View File

@ -0,0 +1,98 @@
/*
* 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
import android.content.Context
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.Configuration
import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.common.DaggerTestMatrixComponent
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
import im.vector.matrix.android.internal.network.UserAgentHolder
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import org.matrix.olm.OlmManager
import java.io.InputStream
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
/**
* This is the main entry point to the matrix sdk.
* To get the singleton instance, use getInstance static method.
*/
class Matrix private constructor(context: Context, matrixConfiguration: MatrixConfiguration) {
@Inject internal lateinit var authenticationService: AuthenticationService
@Inject internal lateinit var userAgentHolder: UserAgentHolder
@Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
@Inject internal lateinit var olmManager: OlmManager
@Inject internal lateinit var sessionManager: SessionManager
init {
Monarchy.init(context)
DaggerTestMatrixComponent.factory().create(context, matrixConfiguration).inject(this)
if (context.applicationContext !is Configuration.Provider) {
WorkManager.initialize(context, Configuration.Builder().setExecutor(Executors.newCachedThreadPool()).build())
}
ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver)
}
fun getUserAgent() = userAgentHolder.userAgent
fun authenticationService(): AuthenticationService {
return authenticationService
}
companion object {
private lateinit var instance: Matrix
private val isInit = AtomicBoolean(false)
fun initialize(context: Context, matrixConfiguration: MatrixConfiguration) {
if (isInit.compareAndSet(false, true)) {
instance = Matrix(context.applicationContext, matrixConfiguration)
}
}
fun getInstance(context: Context): Matrix {
if (isInit.compareAndSet(false, true)) {
val appContext = context.applicationContext
if (appContext is MatrixConfiguration.Provider) {
val matrixConfiguration = (appContext as MatrixConfiguration.Provider).providesMatrixConfiguration()
instance = Matrix(appContext, matrixConfiguration)
} else {
throw IllegalStateException("Matrix is not initialized properly." +
" You should call Matrix.initialize or let your application implements MatrixConfiguration.Provider.")
}
}
return instance
}
fun getSdkVersion(): String {
return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")"
}
fun decryptStream(inputStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? {
return MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
}
}
}

View File

@ -57,9 +57,10 @@ class CommonTestHelper(context: Context) {
val matrix: Matrix
fun getTestInterceptor(session: Session): MockOkHttpInterceptor? = TestNetworkModule.interceptorForSession(session.sessionId) as? MockOkHttpInterceptor
init {
Matrix.initialize(context, MatrixConfiguration("TestFlavor"))
matrix = Matrix.getInstance(context)
}
@ -116,6 +117,7 @@ class CommonTestHelper(context: Context) {
* @param nbOfMessages the number of time the message will be sent
*/
fun sendTextMessage(room: Room, message: String, nbOfMessages: Int): List<TimelineEvent> {
val timeline = room.createTimeline(null, TimelineSettings(10))
val sentEvents = ArrayList<TimelineEvent>(nbOfMessages)
val latch = CountDownLatch(1)
val timelineListener = object : Timeline.Listener {
@ -134,11 +136,12 @@ class CommonTestHelper(context: Context) {
if (newMessages.size == nbOfMessages) {
sentEvents.addAll(newMessages)
// Remove listener now, if not at the next update sendEvents could change
timeline.removeListener(this)
latch.countDown()
}
}
}
val timeline = room.createTimeline(null, TimelineSettings(10))
timeline.start()
timeline.addListener(timelineListener)
for (i in 0 until nbOfMessages) {
@ -146,11 +149,10 @@ class CommonTestHelper(context: Context) {
}
// Wait 3 second more per message
await(latch, timeout = TestConstants.timeOutMillis + 3_000L * nbOfMessages)
timeline.removeListener(timelineListener)
timeline.dispose()
// Check that all events has been created
assertEquals(nbOfMessages.toLong(), sentEvents.size.toLong())
assertEquals("Message number do not match $sentEvents", nbOfMessages.toLong(), sentEvents.size.toLong())
return sentEvents
}

View File

@ -17,8 +17,13 @@
package im.vector.matrix.android.common
import android.os.SystemClock
import android.util.Log
import androidx.lifecycle.Observer
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.OutgoingSasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
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
@ -34,6 +39,7 @@ import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@ -41,6 +47,8 @@ import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import java.util.UUID
import java.util.concurrent.CountDownLatch
class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
@ -57,7 +65,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
val roomId = mTestHelper.doSync<String> {
aliceSession.createRoom(CreateRoomParams(name = "MyRoom"), it)
aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" }, it)
}
if (encryptedRoom) {
@ -167,7 +175,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
}
mTestHelper.doSync<Unit> {
samSession.joinRoom(room.roomId, null, it)
samSession.joinRoom(room.roomId, null, emptyList(), it)
}
return samSession
@ -241,14 +249,14 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
val eventWireContent = event.content.toContent()
assertNotNull(eventWireContent)
assertNull(eventWireContent.get("body"))
assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent.get("algorithm"))
assertNull(eventWireContent["body"])
assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent["algorithm"])
assertNotNull(eventWireContent.get("ciphertext"))
assertNotNull(eventWireContent.get("session_id"))
assertNotNull(eventWireContent.get("sender_key"))
assertNotNull(eventWireContent["ciphertext"])
assertNotNull(eventWireContent["session_id"])
assertNotNull(eventWireContent["sender_key"])
assertEquals(senderSession.sessionParams.deviceId, eventWireContent.get("device_id"))
assertEquals(senderSession.sessionParams.deviceId, eventWireContent["device_id"])
assertNotNull(event.eventId)
assertEquals(roomId, event.roomId)
@ -257,7 +265,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
val eventContent = event.toContent()
assertNotNull(eventContent)
assertEquals(clearMessage, eventContent.get("body"))
assertEquals(clearMessage, eventContent["body"])
assertEquals(senderSession.myUserId, event.senderId)
}
@ -274,4 +282,143 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
authData = createFakeMegolmBackupAuthData()
)
}
fun createDM(alice: Session, bob: Session): String {
val roomId = mTestHelper.doSync<String> {
alice.createRoom(
CreateRoomParams().apply {
invitedUserIds.add(bob.myUserId)
setDirectMessage()
enableEncryptionIfInvitedUsersSupportIt = true
},
it
)
}
mTestHelper.waitWithLatch { latch ->
val bobRoomSummariesLive = runBlocking(Dispatchers.Main) {
bob.getRoomSummariesLive(roomSummaryQueryParams { })
}
val newRoomObserver = object : Observer<List<RoomSummary>> {
override fun onChanged(t: List<RoomSummary>?) {
val indexOfFirst = t?.indexOfFirst { it.roomId == roomId } ?: -1
if (indexOfFirst != -1) {
latch.countDown()
bobRoomSummariesLive.removeObserver(this)
}
}
}
GlobalScope.launch(Dispatchers.Main) {
bobRoomSummariesLive.observeForever(newRoomObserver)
}
}
mTestHelper.waitWithLatch { latch ->
val bobRoomSummariesLive = runBlocking(Dispatchers.Main) {
bob.getRoomSummariesLive(roomSummaryQueryParams { })
}
val newRoomObserver = object : Observer<List<RoomSummary>> {
override fun onChanged(t: List<RoomSummary>?) {
if (bob.getRoom(roomId)
?.getRoomMember(bob.myUserId)
?.membership == Membership.JOIN) {
latch.countDown()
bobRoomSummariesLive.removeObserver(this)
}
}
}
GlobalScope.launch(Dispatchers.Main) {
bobRoomSummariesLive.observeForever(newRoomObserver)
}
mTestHelper.doSync<Unit> { bob.joinRoom(roomId, callback = it) }
}
return roomId
}
fun initializeCrossSigning(session: Session) {
mTestHelper.doSync<Unit> {
session.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = session.myUserId,
password = TestConstants.PASSWORD
), it)
}
}
fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) {
assertTrue(alice.cryptoService().crossSigningService().canCrossSign())
assertTrue(bob.cryptoService().crossSigningService().canCrossSign())
val requestID = UUID.randomUUID().toString()
val aliceVerificationService = alice.cryptoService().verificationService()
val bobVerificationService = bob.cryptoService().verificationService()
aliceVerificationService.beginKeyVerificationInDMs(
VerificationMethod.SAS,
requestID,
roomId,
bob.myUserId,
bob.sessionParams.credentials.deviceId!!,
null)
// we should reach SHOW SAS on both
var alicePovTx: OutgoingSasVerificationTransaction? = null
var bobPovTx: IncomingSasVerificationTransaction? = null
// wait for alice to get the ready
mTestHelper.waitWithLatch {
mTestHelper.retryPeriodicallyWithLatch(it) {
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
if (bobPovTx?.state == VerificationTxState.OnStarted) {
bobPovTx?.performAccept()
true
} else {
false
}
}
}
mTestHelper.waitWithLatch {
mTestHelper.retryPeriodicallyWithLatch(it) {
alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID) as? OutgoingSasVerificationTransaction
Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}")
alicePovTx?.state == VerificationTxState.ShortCodeReady
}
}
// wait for alice to get the ready
mTestHelper.waitWithLatch {
mTestHelper.retryPeriodicallyWithLatch(it) {
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
if (bobPovTx?.state == VerificationTxState.OnStarted) {
bobPovTx?.performAccept()
}
bobPovTx?.state == VerificationTxState.ShortCodeReady
}
}
assertEquals("SAS code do not match", alicePovTx!!.getDecimalCodeRepresentation(), bobPovTx!!.getDecimalCodeRepresentation())
bobPovTx!!.userHasVerifiedShortCode()
alicePovTx!!.userHasVerifiedShortCode()
mTestHelper.waitWithLatch {
mTestHelper.retryPeriodicallyWithLatch(it) {
alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
}
}
mTestHelper.waitWithLatch {
mTestHelper.retryPeriodicallyWithLatch(it) {
alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
}
}
}
}

View File

@ -15,6 +15,7 @@
*/
package im.vector.matrix.android.common
import im.vector.matrix.android.internal.session.TestInterceptor
import okhttp3.Interceptor
import okhttp3.Protocol
import okhttp3.Request
@ -37,7 +38,7 @@ import javax.net.ssl.HttpsURLConnection
* AutoDiscovery().findClientConfig("matrix.org", <callback>)
* </code>
*/
class MockOkHttpInterceptor : Interceptor {
class MockOkHttpInterceptor : TestInterceptor {
private var rules: ArrayList<Rule> = ArrayList()
@ -45,6 +46,12 @@ class MockOkHttpInterceptor : Interceptor {
rules.add(rule)
}
fun clearRules() {
rules.clear()
}
override var sessionId: String? = null
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.common
import android.content.Context
import dagger.BindsInstance
import dagger.Component
import im.vector.matrix.android.api.MatrixConfiguration
import im.vector.matrix.android.internal.auth.AuthModule
import im.vector.matrix.android.internal.di.MatrixComponent
import im.vector.matrix.android.internal.di.MatrixModule
import im.vector.matrix.android.internal.di.MatrixScope
import im.vector.matrix.android.internal.di.NetworkModule
@Component(modules = [TestModule::class, MatrixModule::class, NetworkModule::class, AuthModule::class, TestNetworkModule::class])
@MatrixScope
internal interface TestMatrixComponent : MatrixComponent {
@Component.Factory
interface Factory {
fun create(@BindsInstance context: Context,
@BindsInstance matrixConfiguration: MatrixConfiguration): TestMatrixComponent
}
}

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.android.common
import dagger.Binds
import dagger.Module
import im.vector.matrix.android.internal.di.MatrixComponent
@Module
internal abstract class TestModule {
@Binds
abstract fun providesMatrixComponent(testMatrixComponent: TestMatrixComponent): MatrixComponent
}

View File

@ -0,0 +1,39 @@
/*
* 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.common
import dagger.Module
import dagger.Provides
import im.vector.matrix.android.internal.session.MockHttpInterceptor
import im.vector.matrix.android.internal.session.TestInterceptor
@Module
internal object TestNetworkModule {
val interceptors = ArrayList<TestInterceptor>()
fun interceptorForSession(sessionId: String): TestInterceptor? = interceptors.firstOrNull { it.sessionId == sessionId }
@Provides
@JvmStatic
@MockHttpInterceptor
fun providesTestInterceptor(): TestInterceptor? {
return MockOkHttpInterceptor().also {
interceptors.add(it)
}
}
}

View File

@ -22,10 +22,8 @@ import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import io.realm.Realm
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@ -45,22 +43,22 @@ class CryptoStoreTest : InstrumentedTest {
Realm.init(context())
}
@Test
fun test_metadata_realm_ok() {
val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore()
assertFalse(cryptoStore.hasData())
cryptoStore.open()
assertEquals("deviceId_sample", cryptoStore.getDeviceId())
assertTrue(cryptoStore.hasData())
// Cleanup
cryptoStore.close()
cryptoStore.deleteStore()
}
// @Test
// fun test_metadata_realm_ok() {
// val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore()
//
// assertFalse(cryptoStore.hasData())
//
// cryptoStore.open()
//
// assertEquals("deviceId_sample", cryptoStore.getDeviceId())
//
// assertTrue(cryptoStore.hasData())
//
// // Cleanup
// cryptoStore.close()
// cryptoStore.deleteStore()
// }
@Test
fun test_lastSessionUsed() {

View File

@ -29,6 +29,7 @@ import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.common.CommonTestHelper
import im.vector.matrix.android.common.CryptoTestHelper
import im.vector.matrix.android.common.SessionTestParams
import im.vector.matrix.android.common.TestConstants
import im.vector.matrix.android.internal.crypto.GossipingRequestState
@ -56,6 +57,7 @@ import java.util.concurrent.CountDownLatch
class KeyShareTests : InstrumentedTest {
private val mTestHelper = CommonTestHelper(context())
private val mCryptoTestHelper = CryptoTestHelper(mTestHelper)
@Test
fun test_DoNotSelfShareIfNotTrusted() {
@ -64,7 +66,10 @@ class KeyShareTests : InstrumentedTest {
// Create an encrypted room and add a message
val roomId = mTestHelper.doSync<String> {
aliceSession.createRoom(
CreateRoomParams(RoomDirectoryVisibility.PRIVATE).enableEncryptionWithAlgorithm(true),
CreateRoomParams().apply {
visibility = RoomDirectoryVisibility.PRIVATE
enableEncryption()
},
it
)
}
@ -90,7 +95,7 @@ class KeyShareTests : InstrumentedTest {
} catch (failure: Throwable) {
}
val outgoingRequestBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequest()
val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
// Try to request
aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root)
@ -100,10 +105,10 @@ class KeyShareTests : InstrumentedTest {
var outGoingRequestId: String? = null
mTestHelper.retryPeriodicallyWithLatch(waitLatch) {
aliceSession2.cryptoService().getOutgoingRoomKeyRequest()
aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
.filter { req ->
// filter out request that was known before
!outgoingRequestBefore.any { req.requestId == it.requestId }
!outgoingRequestsBefore.any { req.requestId == it.requestId }
}
.let {
val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId }
@ -115,10 +120,10 @@ class KeyShareTests : InstrumentedTest {
Log.v("TEST", "=======> Outgoing requet Id is $outGoingRequestId")
val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequest()
val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
// We should have a new request
Assert.assertTrue(outgoingRequestAfter.size > outgoingRequestBefore.size)
Assert.assertTrue(outgoingRequestAfter.size > outgoingRequestsBefore.size)
Assert.assertNotNull(outgoingRequestAfter.first { it.sessionId == eventMegolmSessionId })
// The first session should see an incoming request
@ -126,7 +131,7 @@ class KeyShareTests : InstrumentedTest {
mTestHelper.waitWithLatch { latch ->
mTestHelper.retryPeriodicallyWithLatch(latch) {
// DEBUG LOGS
aliceSession.cryptoService().getIncomingRoomKeyRequest().let {
aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
Log.v("TEST", "=========================")
it.forEach { keyRequest ->
@ -135,7 +140,7 @@ class KeyShareTests : InstrumentedTest {
Log.v("TEST", "=========================")
}
val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequest().firstOrNull { it.requestId == outGoingRequestId }
val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
incoming?.state == GossipingRequestState.REJECTED
}
}
@ -155,7 +160,7 @@ class KeyShareTests : InstrumentedTest {
mTestHelper.waitWithLatch { latch ->
mTestHelper.retryPeriodicallyWithLatch(latch) {
aliceSession.cryptoService().getIncomingRoomKeyRequest().let {
aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
Log.v("TEST", "Incoming request Session 1")
Log.v("TEST", "=========================")
it.forEach {
@ -171,7 +176,7 @@ class KeyShareTests : InstrumentedTest {
Thread.sleep(6_000)
mTestHelper.waitWithLatch { latch ->
mTestHelper.retryPeriodicallyWithLatch(latch) {
aliceSession2.cryptoService().getOutgoingRoomKeyRequest().let {
aliceSession2.cryptoService().getOutgoingRoomKeyRequests().let {
it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == OutgoingGossipingRequestState.CANCELLED }
}
}
@ -234,6 +239,7 @@ class KeyShareTests : InstrumentedTest {
}
if (tx.state == VerificationTxState.ShortCodeReady) {
session1ShortCode = tx.getDecimalCodeRepresentation()
Thread.sleep(500)
tx.userHasVerifiedShortCode()
}
}
@ -246,13 +252,14 @@ class KeyShareTests : InstrumentedTest {
if (tx is SasVerificationTransaction) {
if (tx.state == VerificationTxState.ShortCodeReady) {
session2ShortCode = tx.getDecimalCodeRepresentation()
Thread.sleep(500)
tx.userHasVerifiedShortCode()
}
}
}
})
val txId: String = "m.testVerif12"
val txId = "m.testVerif12"
aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.deviceId
?: "", txId)
@ -281,9 +288,12 @@ class KeyShareTests : InstrumentedTest {
mTestHelper.waitWithLatch(60_000) { latch ->
val keysBackupService = aliceSession2.cryptoService().keysBackupService()
mTestHelper.retryPeriodicallyWithLatch(latch) {
Log.d("#TEST", "Recovery :${ keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
Log.d("#TEST", "Recovery :${keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey
}
}
mTestHelper.signOutAndClose(aliceSession1)
mTestHelper.signOutAndClose(aliceSession2)
}
}

View File

@ -0,0 +1,245 @@
/*
* 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.crypto.gossiping
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.NoOpMatrixCallback
import im.vector.matrix.android.api.extensions.tryThis
import im.vector.matrix.android.api.session.crypto.MXCryptoError
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.common.CommonTestHelper
import im.vector.matrix.android.common.CryptoTestHelper
import im.vector.matrix.android.common.MockOkHttpInterceptor
import im.vector.matrix.android.common.SessionTestParams
import im.vector.matrix.android.common.TestConstants
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode
import org.junit.Assert
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class WithHeldTests : InstrumentedTest {
private val mTestHelper = CommonTestHelper(context())
private val mCryptoTestHelper = CryptoTestHelper(mTestHelper)
@Test
fun test_WithHeldUnverifiedReason() {
// =============================
// ARRANGE
// =============================
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
val bobSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
// Initialize cross signing on both
mCryptoTestHelper.initializeCrossSigning(aliceSession)
mCryptoTestHelper.initializeCrossSigning(bobSession)
val roomId = mCryptoTestHelper.createDM(aliceSession, bobSession)
mCryptoTestHelper.verifySASCrossSign(aliceSession, bobSession, roomId)
val roomAlicePOV = aliceSession.getRoom(roomId)!!
val bobUnverifiedSession = mTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
// =============================
// ACT
// =============================
// Alice decide to not send to unverified sessions
aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true)
val timelineEvent = mTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob", 1).first()
// await for bob unverified session to get the message
mTestHelper.waitWithLatch { latch ->
mTestHelper.retryPeriodicallyWithLatch(latch) {
bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(timelineEvent.eventId) != null
}
}
val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(timelineEvent.eventId)!!
// =============================
// ASSERT
// =============================
// Bob should not be able to decrypt because the keys is withheld
try {
// .. might need to wait a bit for stability?
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
Assert.fail("This session should not be able to decrypt")
} catch (failure: Throwable) {
val type = (failure as MXCryptoError.Base).errorType
val technicalMessage = failure.technicalMessage
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
}
// enable back sending to unverified
aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false)
val secondEvent = mTestHelper.sendTextMessage(roomAlicePOV, "Verify your device!!", 1).first()
mTestHelper.waitWithLatch { latch ->
mTestHelper.retryPeriodicallyWithLatch(latch) {
val ev = bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(secondEvent.eventId)
// wait until it's decrypted
ev?.root?.getClearType() == EventType.MESSAGE
}
}
// Previous message should still be undecryptable (partially withheld session)
try {
// .. might need to wait a bit for stability?
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
Assert.fail("This session should not be able to decrypt")
} catch (failure: Throwable) {
val type = (failure as MXCryptoError.Base).errorType
val technicalMessage = failure.technicalMessage
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
}
mTestHelper.signOutAndClose(aliceSession)
mTestHelper.signOutAndClose(bobSession)
mTestHelper.signOutAndClose(bobUnverifiedSession)
}
@Test
fun test_WithHeldNoOlm() {
val testData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = testData.firstSession
val bobSession = testData.secondSession!!
val aliceInterceptor = mTestHelper.getTestInterceptor(aliceSession)
// Simulate no OTK
aliceInterceptor!!.addRule(MockOkHttpInterceptor.SimpleRule(
"/keys/claim",
200,
"""
{ "one_time_keys" : {} }
"""
))
Log.d("#TEST", "Recovery :${aliceSession.sessionParams.credentials.accessToken}")
val roomAlicePov = aliceSession.getRoom(testData.roomId)!!
val eventId = mTestHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId
// await for bob session to get the message
mTestHelper.waitWithLatch { latch ->
mTestHelper.retryPeriodicallyWithLatch(latch) {
bobSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId) != null
}
}
// Previous message should still be undecryptable (partially withheld session)
val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId)
try {
// .. might need to wait a bit for stability?
bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "")
Assert.fail("This session should not be able to decrypt")
} catch (failure: Throwable) {
val type = (failure as MXCryptoError.Base).errorType
val technicalMessage = failure.technicalMessage
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
Assert.assertEquals("Cause should be unverified", WithHeldCode.NO_OLM.value, technicalMessage)
}
// Ensure that alice has marked the session to be shared with bob
val sessionId = eventBobPOV!!.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
val chainIndex = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSession.myUserId, bobSession.sessionParams.credentials.deviceId)
Assert.assertEquals("Alice should have marked bob's device for this session", 0, chainIndex)
// Add a new device for bob
aliceInterceptor.clearRules()
val bobSecondSession = mTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(withInitialSync = true))
// send a second message
val secondMessageId = mTestHelper.sendTextMessage(roomAlicePov, "second message", 1).first().eventId
// Check that the
// await for bob SecondSession session to get the message
mTestHelper.waitWithLatch { latch ->
mTestHelper.retryPeriodicallyWithLatch(latch) {
bobSecondSession.getRoom(testData.roomId)?.getTimeLineEvent(secondMessageId) != null
}
}
val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSecondSession.myUserId, bobSecondSession.sessionParams.credentials.deviceId)
Assert.assertEquals("Alice should have marked bob's device for this session", 1, chainIndex2)
aliceInterceptor.clearRules()
testData.cleanUp(mTestHelper)
mTestHelper.signOutAndClose(bobSecondSession)
}
@Test
fun test_WithHeldKeyRequest() {
val testData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = testData.firstSession
val bobSession = testData.secondSession!!
val roomAlicePov = aliceSession.getRoom(testData.roomId)!!
val eventId = mTestHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId
mTestHelper.signOutAndClose(bobSession)
// Create a new session for bob
val bobSecondSession = mTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
// initialize to force request keys if missing
mCryptoTestHelper.initializeCrossSigning(bobSecondSession)
// Trust bob second device from Alice POV
aliceSession.cryptoService().crossSigningService().trustDevice(bobSecondSession.sessionParams.deviceId!!, NoOpMatrixCallback())
bobSecondSession.cryptoService().crossSigningService().trustDevice(aliceSession.sessionParams.deviceId!!, NoOpMatrixCallback())
var sessionId: String? = null
// Check that the
// await for bob SecondSession session to get the message
mTestHelper.waitWithLatch { latch ->
mTestHelper.retryPeriodicallyWithLatch(latch) {
val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId)?.also {
// try to decrypt and force key request
tryThis { bobSecondSession.cryptoService().decryptEvent(it.root, "") }
}
sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId
timeLineEvent != null
}
}
// Check that bob second session requested the key
mTestHelper.waitWithLatch { latch ->
mTestHelper.retryPeriodicallyWithLatch(latch) {
val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!)
wc?.code == WithHeldCode.UNAUTHORISED
}
}
}
}

View File

@ -144,7 +144,7 @@ class QuadSTests : InstrumentedTest {
val secretAccountData = assertAccountData(aliceSession, "secret.of.life")
val encryptedContent = secretAccountData.content.get("encrypted") as? Map<*, *>
val encryptedContent = secretAccountData.content["encrypted"] as? Map<*, *>
assertNotNull("Element should be encrypted", encryptedContent)
assertNotNull("Secret should be encrypted with default key", encryptedContent?.get(keyId))

View File

@ -59,7 +59,7 @@ internal class ChunkEntityTest : InstrumentedTest {
monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED).let {
val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let {
realm.copyToRealmOrUpdate(it)
}
chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
@ -71,7 +71,7 @@ internal class ChunkEntityTest : InstrumentedTest {
fun add_shouldNotAdd_whenAlreadyIncluded() {
monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED).let {
val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let {
realm.copyToRealmOrUpdate(it)
}
chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
@ -141,7 +141,7 @@ internal class ChunkEntityTest : InstrumentedTest {
events: List<Event>,
direction: PaginationDirection) {
events.forEach { event ->
val fakeEvent = event.toEntity(roomId, SendState.SYNCED).let {
val fakeEvent = event.toEntity(roomId, SendState.SYNCED, System.currentTimeMillis()).let {
realm.copyToRealmOrUpdate(it)
}
addTimelineEvent(roomId, fakeEvent, direction, emptyMap())

View File

@ -0,0 +1,68 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database
import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntity
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntity
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntity
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntity
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.kotlin.where
import timber.log.Timber
object RealmDebugTools {
/**
* Log info about the crypto DB
*/
fun dumpCryptoDb(realmConfiguration: RealmConfiguration) {
Realm.getInstance(realmConfiguration).use {
Timber.d("Realm located at : ${realmConfiguration.realmDirectory}/${realmConfiguration.realmFileName}")
val key = realmConfiguration.encryptionKey.joinToString("") { byte -> "%02x".format(byte) }
Timber.d("Realm encryption key : $key")
// Check if we have data
Timber.e("Realm is empty: ${it.isEmpty}")
Timber.d("Realm has CryptoMetadataEntity: ${it.where<CryptoMetadataEntity>().count()}")
Timber.d("Realm has CryptoRoomEntity: ${it.where<CryptoRoomEntity>().count()}")
Timber.d("Realm has DeviceInfoEntity: ${it.where<DeviceInfoEntity>().count()}")
Timber.d("Realm has KeysBackupDataEntity: ${it.where<KeysBackupDataEntity>().count()}")
Timber.d("Realm has OlmInboundGroupSessionEntity: ${it.where<OlmInboundGroupSessionEntity>().count()}")
Timber.d("Realm has OlmSessionEntity: ${it.where<OlmSessionEntity>().count()}")
Timber.d("Realm has UserEntity: ${it.where<UserEntity>().count()}")
Timber.d("Realm has KeyInfoEntity: ${it.where<KeyInfoEntity>().count()}")
Timber.d("Realm has CrossSigningInfoEntity: ${it.where<CrossSigningInfoEntity>().count()}")
Timber.d("Realm has TrustLevelEntity: ${it.where<TrustLevelEntity>().count()}")
Timber.d("Realm has GossipingEventEntity: ${it.where<GossipingEventEntity>().count()}")
Timber.d("Realm has IncomingGossipingRequestEntity: ${it.where<IncomingGossipingRequestEntity>().count()}")
Timber.d("Realm has OutgoingGossipingRequestEntity: ${it.where<OutgoingGossipingRequestEntity>().count()}")
Timber.d("Realm has MyDeviceLastSeenInfoEntity: ${it.where<MyDeviceLastSeenInfoEntity>().count()}")
}
}
}

View File

@ -1,5 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="im.vector.matrix.android">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@ -8,13 +7,20 @@
<application android:networkSecurityConfig="@xml/network_security_config">
<!--
The SDK offers a secured File provider to access downloaded files.
Access to these file will be given via the FileService, with a temporary
read access permission
-->
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
android:name="im.vector.matrix.android.api.session.file.MatrixSDKFileProvider"
android:authorities="${applicationId}.mx-sdk.fileprovider"
android:exported="false"
tools:node="remove" />
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/sdk_provider_paths" />
</provider>
</application>
</manifest>

View File

@ -23,6 +23,7 @@ import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.legacy.LegacySessionImporter
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
@ -41,6 +42,7 @@ import javax.inject.Inject
*/
class Matrix private constructor(context: Context, matrixConfiguration: MatrixConfiguration) {
@Inject internal lateinit var legacySessionImporter: LegacySessionImporter
@Inject internal lateinit var authenticationService: AuthenticationService
@Inject internal lateinit var userAgentHolder: UserAgentHolder
@Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
@ -62,6 +64,10 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
return authenticationService
}
fun legacySessionImporter(): LegacySessionImporter {
return legacySessionImporter
}
companion object {
private lateinit var instance: Matrix

View File

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

View File

@ -89,6 +89,7 @@ interface AuthenticationService {
* Perform a wellknown request, using the domain from the matrixId
*/
fun getWellKnownData(matrixId: String,
homeServerConnectionConfig: HomeServerConnectionConfig?,
callback: MatrixCallback<WellknownResult>): Cancelable
/**

View File

@ -34,10 +34,10 @@ data class HomeServerConnectionConfig(
val homeServerUri: Uri,
val identityServerUri: Uri? = null,
val antiVirusServerUri: Uri? = null,
val allowedFingerprints: MutableList<Fingerprint> = ArrayList(),
val allowedFingerprints: List<Fingerprint> = emptyList(),
val shouldPin: Boolean = false,
val tlsVersions: MutableList<TlsVersion>? = null,
val tlsCipherSuites: MutableList<CipherSuite>? = null,
val tlsVersions: List<TlsVersion>? = null,
val tlsCipherSuites: List<CipherSuite>? = null,
val shouldAcceptTlsExtensions: Boolean = true,
val allowHttpExtension: Boolean = false,
val forceUsageTlsVersions: Boolean = false
@ -69,7 +69,7 @@ data class HomeServerConnectionConfig(
*/
fun withHomeServerUri(hsUri: Uri): Builder {
if (hsUri.scheme != "http" && hsUri.scheme != "https") {
throw RuntimeException("Invalid home server URI: " + hsUri)
throw RuntimeException("Invalid home server URI: $hsUri")
}
// ensure trailing /
val hsString = hsUri.toString().ensureTrailingSlash()

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

@ -16,10 +16,15 @@
package im.vector.matrix.android.api.extensions
inline fun <A> tryThis(operation: () -> A): A? {
import timber.log.Timber
inline fun <A> tryThis(message: String? = null, operation: () -> A): A? {
return try {
operation()
} catch (any: Throwable) {
if (message != null) {
Timber.e(any, message)
}
null
}
}

View File

@ -18,6 +18,7 @@ package im.vector.matrix.android.api.failure
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
import im.vector.matrix.android.internal.network.ssl.Fingerprint
import java.io.IOException
/**
@ -32,9 +33,11 @@ import java.io.IOException
sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
data class Unknown(val throwable: Throwable? = null) : Failure(throwable)
data class Cancelled(val throwable: Throwable? = null) : Failure(throwable)
data class UnrecognizedCertificateFailure(val url: String, val fingerprint: Fingerprint) : Failure()
data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException)
data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString()))
object SuccessError : Failure(RuntimeException(RuntimeException("SuccessResult is false")))
// When server send an error, but it cannot be interpreted as a MatrixError
data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException("HTTP $httpCode: $errorBody"))

View File

@ -16,8 +16,11 @@
package im.vector.matrix.android.api.failure
import im.vector.matrix.android.internal.network.ssl.Fingerprint
// This class will be sent to the bus
sealed class GlobalError {
data class InvalidToken(val softLogout: Boolean) : GlobalError()
data class ConsentNotGivenError(val consentUri: String) : GlobalError()
data class CertificateError(val fingerprint: Fingerprint) : GlobalError()
}

View File

@ -0,0 +1,26 @@
/*
* 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.legacy
interface LegacySessionImporter {
/**
* Will eventually import a session created by the legacy app.
* @return true if a session has been imported
*/
fun process(): Boolean
}

View File

@ -87,14 +87,13 @@ class EventMatchCondition(
// Very simple glob to regexp converter
private fun simpleGlobToRegExp(glob: String): String {
var out = "" // "^"
for (i in 0 until glob.length) {
val c = glob[i]
when (c) {
for (element in glob) {
when (element) {
'*' -> out += ".*"
'?' -> out += '.'.toString()
'.' -> out += "\\."
'\\' -> out += "\\\\"
else -> out += c
else -> out += element
}
}
out += "" // '$'.toString()

View File

@ -20,10 +20,13 @@ import androidx.lifecycle.LiveData
interface InitialSyncProgressService {
fun getInitialSyncProgressStatus() : LiveData<Status?>
fun getInitialSyncProgressStatus(): LiveData<Status>
data class Status(
@StringRes val statusText: Int,
val percentProgress: Int = 0
)
sealed class Status {
object Idle : Status()
data class Progressing(
@StringRes val statusText: Int,
val percentProgress: Int = 0
) : Status()
}
}

View File

@ -24,9 +24,11 @@ import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.session.account.AccountService
import im.vector.matrix.android.api.session.accountdata.AccountDataService
import im.vector.matrix.android.api.session.cache.CacheService
import im.vector.matrix.android.api.session.call.CallSignalingService
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.file.ContentDownloadStateTracker
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
@ -42,8 +44,10 @@ import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.session.typing.TypingUsersTracker
import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.api.session.widgets.WidgetService
import okhttp3.OkHttpClient
/**
* This interface defines interactions with a session.
@ -57,7 +61,6 @@ interface Session :
CacheService,
SignOutService,
FilterService,
FileService,
TermsService,
ProfileService,
PushRuleService,
@ -125,6 +128,12 @@ interface Session :
*/
fun getSyncStateLive(): LiveData<SyncState>
/**
* This method returns the current sync state.
* @return the current [SyncState].
*/
fun getSyncState(): SyncState
/**
* This methods return true if an initial sync has been processed
*/
@ -145,6 +154,16 @@ interface Session :
*/
fun contentUploadProgressTracker(): ContentUploadStateTracker
/**
* Returns the TypingUsersTracker associated with the session
*/
fun typingUsersTracker(): TypingUsersTracker
/**
* Returns the ContentDownloadStateTracker associated with the session
*/
fun contentDownloadProgressTracker(): ContentDownloadStateTracker
/**
* Returns the cryptoService associated with the session
*/
@ -165,6 +184,16 @@ interface Session :
*/
fun integrationManagerService(): IntegrationManagerService
/**
* Returns the call signaling service associated with the session
*/
fun callSignalingService(): CallSignalingService
/**
* Returns the file download service associated with the session
*/
fun fileService(): FileService
/**
* Add a listener to the session.
* @param listener the listener to add.
@ -177,6 +206,13 @@ interface Session :
*/
fun removeListener(listener: Listener)
/**
* Will return a OkHttpClient which will manage pinned certificates and Proxy if configured.
* It will not add any access-token to the request.
* So it is exposed to let the app be able to download image with Glide or any other libraries which accept an OkHttp client.
*/
fun getOkHttpClient(): OkHttpClient
/**
* A global session listener to get notified for some events.
*/

View File

@ -0,0 +1,36 @@
/*
* 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.call
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
interface CallSignalingService {
fun getTurnServer(callback: MatrixCallback<TurnServerResponse>): Cancelable
/**
* Create an outgoing call
*/
fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall
fun addCallListener(listener: CallsListener)
fun removeCallListener(listener: CallsListener)
fun getCallWithId(callId: String) : MxCall?
}

View File

@ -0,0 +1,44 @@
/*
* 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.call
import org.webrtc.PeerConnection
sealed class CallState {
/** Idle, setting up objects */
object Idle : CallState()
/** Dialing. Outgoing call is signaling the remote peer */
object Dialing : CallState()
/** Local ringing. Incoming call offer received */
object LocalRinging : CallState()
/** Answering. Incoming call is responding to remote peer */
object Answering : CallState()
/**
* Connected. Incoming/Outgoing call, ice layer connecting or connected
* Notice that the PeerState failed is not always final, if you switch network, new ice candidtates
* could be exchanged, and the connection could go back to connected
* */
data class Connected(val iceConnectionState: PeerConnection.PeerConnectionState) : CallState()
/** Terminated. Incoming/Outgoing call, the call is terminated */
object Terminated : CallState()
}

View File

@ -0,0 +1,43 @@
/*
* 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.call
import im.vector.matrix.android.api.session.room.model.call.CallAnswerContent
import im.vector.matrix.android.api.session.room.model.call.CallCandidatesContent
import im.vector.matrix.android.api.session.room.model.call.CallHangupContent
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
interface CallsListener {
/**
* Called when there is an incoming call within the room.
*/
fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent)
fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent)
/**
* An outgoing call is started.
*/
fun onCallAnswerReceived(callAnswerContent: CallAnswerContent)
/**
* Called when a called has been hung up
*/
fun onCallHangupReceived(callHangupContent: CallHangupContent)
fun onCallManagedByOtherSession(callId: String)
}

View File

@ -0,0 +1,55 @@
/*
* 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.call
import org.webrtc.EglBase
import timber.log.Timber
/**
* The root [EglBase] instance shared by the entire application for
* the sake of reducing the utilization of system resources (such as EGL
* contexts)
* by performing a runtime check.
*/
object EglUtils {
// TODO how do we release that?
/**
* Lazily creates and returns the one and only [EglBase] which will
* serve as the root for all contexts that are needed.
*/
@get:Synchronized var rootEglBase: EglBase? = null
get() {
if (field == null) {
val configAttributes = EglBase.CONFIG_PLAIN
try {
field = EglBase.createEgl14(configAttributes)
?: EglBase.createEgl10(configAttributes) // Fall back to EglBase10.
} catch (ex: Throwable) {
Timber.e(ex, "Failed to create EglBase")
}
}
return field
}
private set
val rootEglBaseContext: EglBase.Context?
get() {
val eglBase = rootEglBase
return eglBase?.eglBaseContext
}
}

View File

@ -0,0 +1,75 @@
/*
* 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.call
import org.webrtc.IceCandidate
import org.webrtc.SessionDescription
interface MxCallDetail {
val callId: String
val isOutgoing: Boolean
val roomId: String
val otherUserId: String
val isVideoCall: Boolean
}
/**
* Define both an incoming call and on outgoing call
*/
interface MxCall : MxCallDetail {
var state: CallState
/**
* Pick Up the incoming call
* It has no effect on outgoing call
*/
fun accept(sdp: SessionDescription)
/**
* Reject an incoming call
* It's an alias to hangUp
*/
fun reject() = hangUp()
/**
* End the call
*/
fun hangUp()
/**
* Start a call
* Send offer SDP to the other participant.
*/
fun offerSdp(sdp: SessionDescription)
/**
* Send Ice candidate to the other participant.
*/
fun sendLocalIceCandidates(candidates: List<IceCandidate>)
/**
* Send removed ICE candidates to the other participant.
*/
fun sendLocalIceCandidateRemovals(candidates: List<IceCandidate>)
fun addListener(listener: StateListener)
fun removeListener(listener: StateListener)
interface StateListener {
fun onStateUpdate(call: MxCall)
}
}

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.matrix.android.api.session.call
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// TODO Should not be exposed
/**
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-voip-turnserver
*/
@JsonClass(generateAdapter = true)
data class TurnServerResponse(
/**
* Required. The username to use.
*/
@Json(name = "username") val username: String?,
/**
* Required. The password to use.
*/
@Json(name = "password") val password: String?,
/**
* Required. A list of TURN URIs
*/
@Json(name = "uris") val uris: List<String>?,
/**
* Required. The time-to-live in seconds
*/
@Json(name = "ttl") val ttl: Int?
)

View File

@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
@ -99,7 +100,9 @@ interface CryptoService {
fun removeRoomKeysRequestListener(listener: GossipingRequestListener)
fun fetchDevicesList(callback: MatrixCallback<DevicesListResponse>)
fun getMyDevicesInfo() : List<DeviceInfo>
fun getLiveMyDevicesInfo() : LiveData<List<DeviceInfo>>
fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>)
@ -138,7 +141,13 @@ interface CryptoService {
fun removeSessionListener(listener: NewSessionListener)
fun getOutgoingRoomKeyRequest(): List<OutgoingRoomKeyRequest>
fun getIncomingRoomKeyRequest(): List<IncomingRoomKeyRequest>
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
fun getGossipingEventsTrail(): List<Event>
// For testing shared session
fun getSharedWithInfo(roomId: String?, sessionId: String) : MXUsersDevicesMap<Int>
fun getWithHeldMegolmSession(roomId: String, sessionId: String) : RoomKeyWithHeldContent?
}

View File

@ -59,7 +59,8 @@ sealed class MXCryptoError : Throwable() {
MISSING_PROPERTY,
OLM,
UNKNOWN_DEVICES,
UNKNOWN_MESSAGE_INDEX
UNKNOWN_MESSAGE_INDEX,
KEYS_WITHHELD
}
companion object {

View File

@ -41,11 +41,13 @@ interface CrossSigningService {
* Users needs to enter credentials
*/
fun initializeCrossSigning(authParams: UserPasswordAuth?,
callback: MatrixCallback<Unit>? = null)
callback: MatrixCallback<Unit>)
fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null
fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?,
uskKeyPrivateKey: String?,
sskPrivateKey: String?) : UserTrustResult
sskPrivateKey: String?): UserTrustResult
fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo?
@ -59,6 +61,8 @@ interface CrossSigningService {
fun canCrossSign(): Boolean
fun allPrivateKeysKnown(): Boolean
fun trustUser(otherUserId: String,
callback: MatrixCallback<Unit>)
@ -74,6 +78,8 @@ interface CrossSigningService {
otherDeviceId: String,
locallyTrusted: Boolean?): DeviceTrustResult
// FIXME Those method do not have to be in the service
fun onSecretMSKGossip(mskPrivateKey: String)
fun onSecretSSKGossip(sskPrivateKey: String)
fun onSecretUSKGossip(uskPrivateKey: String)
}

View File

@ -82,6 +82,9 @@ data class Event(
@Transient
var mCryptoError: MXCryptoError.ErrorType? = null
@Transient
var mCryptoErrorReason: String? = null
@Transient
var sendState: SendState = SendState.UNKNOWN
@ -182,6 +185,7 @@ data class Event(
if (redacts != other.redacts) return false
if (mxDecryptionResult != other.mxDecryptionResult) return false
if (mCryptoError != other.mCryptoError) return false
if (mCryptoErrorReason != other.mCryptoErrorReason) return false
if (sendState != other.sendState) return false
return true
@ -200,6 +204,7 @@ data class Event(
result = 31 * result + (redacts?.hashCode() ?: 0)
result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0)
result = 31 * result + (mCryptoError?.hashCode() ?: 0)
result = 31 * result + (mCryptoErrorReason?.hashCode() ?: 0)
result = 31 * result + sendState.hashCode()
return result
}
@ -230,3 +235,11 @@ fun Event.isVideoMessage(): Boolean {
else -> false
}
}
fun Event.isFileMessage(): Boolean {
return getClearType() == EventType.MESSAGE
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
MessageType.MSGTYPE_FILE -> true
else -> false
}
}

View File

@ -58,7 +58,6 @@ object EventType {
const val STATE_ROOM_ENCRYPTION = "m.room.encryption"
// Call Events
const val CALL_INVITE = "m.call.invite"
const val CALL_CANDIDATES = "m.call.candidates"
const val CALL_ANSWER = "m.call.answer"
@ -67,6 +66,7 @@ object EventType {
// Key share events
const val ROOM_KEY_REQUEST = "m.room_key_request"
const val FORWARDED_ROOM_KEY = "m.forwarded_room_key"
const val ROOM_KEY_WITHHELD = "org.matrix.room_key.withheld"
const val REQUEST_SECRET = "m.secret.request"
const val SEND_SECRET = "m.secret.send"

View File

@ -26,5 +26,5 @@ object RelationType {
/** Lets you define an event which references an existing event.*/
const val REFERENCE = "m.reference"
/** Lets you define an event which adds a response to an existing event.*/
const val RESPONSE = "m.response"
const val RESPONSE = "org.matrix.response"
}

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

@ -0,0 +1,35 @@
/*
* 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.file
interface ContentDownloadStateTracker {
fun track(key: String, updateListener: UpdateListener)
fun unTrack(key: String, updateListener: UpdateListener)
fun clear()
sealed class State {
object Idle : State()
data class Downloading(val current: Long, val total: Long, val indeterminate: Boolean) : State()
object Decrypting : State()
object Success : State()
data class Failure(val errorCode: Int) : State()
}
interface UpdateListener {
fun onDownloadStateUpdate(state: State)
}
}

View File

@ -16,6 +16,7 @@
package im.vector.matrix.android.api.session.file
import android.net.Uri
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
@ -31,26 +32,58 @@ interface FileService {
* Download file in external storage
*/
TO_EXPORT,
/**
* Download file in cache
*/
FOR_INTERNAL_USE,
/**
* Download file in file provider path
*/
FOR_EXTERNAL_SHARE
}
enum class FileState {
IN_CACHE,
DOWNLOADING,
UNKNOWN
}
/**
* Download a file.
* Result will be a decrypted file, stored in the cache folder. id parameter will be used to create a sub folder to avoid name collision.
* You can pass the eventId
* Result will be a decrypted file, stored in the cache folder. url parameter will be used to create unique filename to avoid name collision.
*/
fun downloadFile(
downloadMode: DownloadMode,
id: String,
fileName: String,
mimeType: String?,
url: String?,
elementToDecrypt: ElementToDecrypt?,
callback: MatrixCallback<File>): Cancelable
fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean
/**
* Use this URI and pass it to intent using flag Intent.FLAG_GRANT_READ_URI_PERMISSION
* (if not other app won't be able to access it)
*/
fun getTemporarySharableURI(mxcUrl: String, mimeType: String?): Uri?
/**
* Get information on the given file.
* Mimetype should be the same one as passed to downloadFile (limitation for now)
*/
fun fileState(mxcUrl: String, mimeType: String?): FileState
/**
* Clears all the files downloaded by the service
*/
fun clearCache()
/**
* Get size of cached files
*/
fun getCacheSize(): Int
}

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.matrix.android.api.session.file
import android.net.Uri
import androidx.core.content.FileProvider
/**
* We have to declare our own file provider to avoid collision with apps using the sdk
* and having their own
*/
class MatrixSDKFileProvider : FileProvider() {
override fun getType(uri: Uri): String? {
return super.getType(uri) ?: "plain/text"
}
}

View File

@ -16,9 +16,20 @@
package im.vector.matrix.android.api.session.group
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
/**
* This interface defines methods to interact within a group.
*/
interface Group {
val groupId: String
/**
* This methods allows you to refresh data about this group. It will be reflected on the GroupSummary.
* The SDK also takes care of refreshing group data every hour.
* @param callback : the matrix callback to be notified of success or failure
* @return a Cancelable to be able to cancel requests.
*/
fun fetchGroupData(callback: MatrixCallback<Unit>): Cancelable
}

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.api.session.profile
import android.net.Uri
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.identity.ThreePid
@ -35,12 +36,27 @@ interface ProfileService {
}
/**
* Return the current dispayname for this user
* Return the current display name for this user
* @param userId the userId param to look for
*
*/
fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable
/**
* Update the display name for this user
* @param userId the userId to update the display name of
* @param newDisplayName the new display name of the user
*/
fun setDisplayName(userId: String, newDisplayName: String, matrixCallback: MatrixCallback<Unit>): Cancelable
/**
* Update the avatar for this user
* @param userId the userId to update the avatar of
* @param newAvatarUri the new avatar uri of the user
* @param fileName the fileName of selected image
*/
fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback<Unit>): Cancelable
/**
* Return the current avatarUrl for this user.
* @param userId the userId param to look for

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.api.session.room
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.session.room.call.RoomCallService
import im.vector.matrix.android.api.session.room.crypto.RoomCryptoService
import im.vector.matrix.android.api.session.room.members.MembershipService
import im.vector.matrix.android.api.session.room.model.RoomSummary
@ -47,6 +48,7 @@ interface Room :
StateService,
UploadsService,
ReportingService,
RoomCallService,
RelationService,
RoomCryptoService,
RoomPushRuleService {

View File

@ -34,13 +34,6 @@ interface RoomDirectoryService {
publicRoomsParams: PublicRoomsParams,
callback: MatrixCallback<PublicRoomsResponse>): Cancelable
/**
* Join a room by id, or room alias
*/
fun joinRoom(roomIdOrAlias: String,
reason: String? = null,
callback: MatrixCallback<Unit>): Cancelable
/**
* Fetches the overall metadata about protocols supported by the homeserver.
* Includes both the available protocols and all fields required for queries against each protocol.

View File

@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.util.Cancelable
@ -104,5 +105,13 @@ interface RoomService {
searchOnServer: Boolean,
callback: MatrixCallback<Optional<String>>): Cancelable
fun getExistingDirectRoomWithUser(otherUserId: String) : Room?
/**
* Return a live data of all local changes membership that happened since the session has been opened.
* It allows you to track this in your client to known what is currently being processed by the SDK.
* It won't know anything about change being done in other client.
* Keys are roomId or roomAlias, depending of what you used as parameter for the join/leave action
*/
fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>>
fun getExistingDirectRoomWithUser(otherUserId: String): Room?
}

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]
*/
data class RoomSummaryQueryParams(
val roomId: QueryStringValue,
val displayName: QueryStringValue,
val canonicalAlias: QueryStringValue,
val memberships: List<Membership>
@ -35,11 +36,13 @@ data class RoomSummaryQueryParams(
class Builder {
var roomId: QueryStringValue = QueryStringValue.IsNotEmpty
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition
var memberships: List<Membership> = Membership.all()
fun build() = RoomSummaryQueryParams(
roomId = roomId,
displayName = displayName,
canonicalAlias = canonicalAlias,
memberships = memberships

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.android.api.session.room.call
/**
* This interface defines methods to handle calls in a room. It's implemented at the room level.
*/
interface RoomCallService {
/**
* Return true if calls (audio or video) can be performed on this Room
*/
fun canStartCall(): Boolean
}

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 im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
import im.vector.matrix.android.api.util.Cancelable
@ -63,6 +64,12 @@ interface MembershipService {
reason: String? = null,
callback: MatrixCallback<Unit>): Cancelable
/**
* Invite a user with email or phone number in the room
*/
fun invite3pid(threePid: ThreePid,
callback: MatrixCallback<Unit>): Cancelable
/**
* Ban a user from the room
*/

View File

@ -17,8 +17,8 @@ package im.vector.matrix.android.api.session.room.model
data class EventAnnotationsSummary(
var eventId: String,
var reactionsSummary: List<ReactionAggregatedSummary>,
var editSummary: EditAggregatedSummary?,
var pollResponseSummary: PollResponseAggregatedSummary?,
var reactionsSummary: List<ReactionAggregatedSummary> = emptyList(),
var editSummary: EditAggregatedSummary? = null,
var pollResponseSummary: PollResponseAggregatedSummary? = null,
var referencesAggregatedSummary: ReferencesAggregatedSummary? = null
)

View File

@ -17,10 +17,12 @@
package im.vector.matrix.android.api.session.room.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Represents the membership of a user on a room
*/
@JsonClass(generateAdapter = false)
enum class Membership(val value: String) {
NONE("none"),

View File

@ -35,4 +35,18 @@ data class PowerLevelsContent(
@Json(name = "users") val users: MutableMap<String, Int> = HashMap(),
@Json(name = "state_default") val stateDefault: Int = Role.Moderator.value,
@Json(name = "notifications") val notifications: Map<String, Any> = HashMap()
)
) {
/**
* Alter this content with a new power level for the specified user
*
* @param userId the userId to alter the power level of
* @param powerLevel the new power level, or null to set the default value.
*/
fun setUserPowerLevel(userId: String, powerLevel: Int?) {
if (powerLevel == null || powerLevel == usersDefault) {
users.remove(userId)
} else {
users[userId] = powerLevel
}
}
}

View File

@ -17,7 +17,9 @@
package im.vector.matrix.android.api.session.room.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = false)
enum class RoomDirectoryVisibility {
@Json(name = "private") PRIVATE,
@Json(name = "public") PUBLIC

View File

@ -29,6 +29,7 @@ data class RoomGuestAccessContent(
@Json(name = "guest_access") val guestAccess: GuestAccess? = null
)
@JsonClass(generateAdapter = false)
enum class GuestAccess(val value: String) {
@Json(name = "can_join")
CanJoin("can_join"),

View File

@ -17,10 +17,12 @@
package im.vector.matrix.android.api.session.room.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Ref: https://matrix.org/docs/spec/client_server/latest#room-history-visibility
*/
@JsonClass(generateAdapter = false)
enum class RoomHistoryVisibility {
/**
* All events while this is the m.room.history_visibility value may be shared by any

View File

@ -18,10 +18,12 @@
package im.vector.matrix.android.api.session.room.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Enum for [RoomJoinRulesContent] : https://matrix.org/docs/spec/client_server/r0.4.0#m-room-join-rules
*/
@JsonClass(generateAdapter = false)
enum class RoomJoinRules(val value: String) {
@Json(name = "public")

View File

@ -19,6 +19,7 @@ package im.vector.matrix.android.api.session.room.model
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.matrix.android.api.session.room.send.UserDraft
import im.vector.matrix.android.api.session.room.sender.SenderInfo
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
/**
@ -27,7 +28,9 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
*/
data class RoomSummary constructor(
val roomId: String,
// Computed display name
val displayName: String = "",
val name: String = "",
val topic: String = "",
val avatarUrl: String = "",
val canonicalAlias: String? = null,
@ -47,8 +50,8 @@ data class RoomSummary constructor(
val userDrafts: List<UserDraft> = emptyList(),
val isEncrypted: Boolean,
val encryptionEventTs: Long?,
val typingUsers: List<SenderInfo>,
val inviterId: String? = null,
val typingRoomMemberIds: List<String> = emptyList(),
val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
) {
@ -62,6 +65,9 @@ data class RoomSummary constructor(
val isFavorite: Boolean
get() = tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE }
val canStartCall: Boolean
get() = isDirect && joinedMembersCount == 2
companion object {
const val NOT_IN_BREADCRUMBS = -1
}

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

@ -19,16 +19,34 @@ package im.vector.matrix.android.api.session.room.model.call
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* This event is sent by the callee when they wish to answer the call.
*/
@JsonClass(generateAdapter = true)
data class CallAnswerContent(
/**
* Required. The ID of the call this event relates to.
*/
@Json(name = "call_id") val callId: String,
@Json(name = "version") val version: Int,
@Json(name = "answer") val answer: Answer
/**
* Required. The session description object
*/
@Json(name = "answer") val answer: Answer,
/**
* Required. The version of the VoIP specification this messages adheres to. This specification is version 0.
*/
@Json(name = "version") val version: Int = 0
) {
@JsonClass(generateAdapter = true)
data class Answer(
@Json(name = "type") val type: String,
/**
* Required. The type of session description. Must be 'answer'.
*/
@Json(name = "type") val type: SdpType = SdpType.ANSWER,
/**
* Required. The SDP text of the session description.
*/
@Json(name = "sdp") val sdp: String
)
}

View File

@ -19,17 +19,39 @@ package im.vector.matrix.android.api.session.room.model.call
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* This event is sent by callers after sending an invite and by the callee after answering.
* Its purpose is to give the other party additional ICE candidates to try using to communicate.
*/
@JsonClass(generateAdapter = true)
data class CallCandidatesContent(
/**
* Required. The ID of the call this event relates to.
*/
@Json(name = "call_id") val callId: String,
@Json(name = "version") val version: Int,
@Json(name = "candidates") val candidates: List<Candidate> = emptyList()
/**
* Required. Array of objects describing the candidates.
*/
@Json(name = "candidates") val candidates: List<Candidate> = emptyList(),
/**
* Required. The version of the VoIP specification this messages adheres to. This specification is version 0.
*/
@Json(name = "version") val version: Int = 0
) {
@JsonClass(generateAdapter = true)
data class Candidate(
/**
* Required. The SDP media type this candidate is intended for.
*/
@Json(name = "sdpMid") val sdpMid: String,
@Json(name = "sdpMLineIndex") val sdpMLineIndex: String,
/**
* Required. The index of the SDP 'm' line this candidate is intended for.
*/
@Json(name = "sdpMLineIndex") val sdpMLineIndex: Int,
/**
* Required. The SDP 'a' line of the candidate.
*/
@Json(name = "candidate") val candidate: String
)
}

View File

@ -19,8 +19,33 @@ package im.vector.matrix.android.api.session.room.model.call
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Sent by either party to signal their termination of the call. This can be sent either once
* the call has been established or before to abort the call.
*/
@JsonClass(generateAdapter = true)
data class CallHangupContent(
/**
* Required. The ID of the call this event relates to.
*/
@Json(name = "call_id") val callId: String,
@Json(name = "version") val version: Int
)
/**
* Required. The version of the VoIP specification this message adheres to. This specification is version 0.
*/
@Json(name = "version") val version: Int = 0,
/**
* Optional error reason for the hangup. This should not be provided when the user naturally ends or rejects the call.
* When there was an error in the call negotiation, this should be `ice_failed` for when ICE negotiation fails
* or `invite_timeout` for when the other party did not answer in time. One of: ["ice_failed", "invite_timeout"]
*/
@Json(name = "reason") val reason: Reason? = null
) {
@JsonClass(generateAdapter = false)
enum class Reason {
@Json(name = "ice_failed")
ICE_FAILED,
@Json(name = "invite_timeout")
INVITE_TIMEOUT
}
}

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