Merge branch 'develop' into feature/fga/voip_v1_start

This commit is contained in:
ganfra 2020-12-22 11:38:41 +01:00
commit 14288b545b
497 changed files with 9434 additions and 5212 deletions

View File

@ -24,6 +24,8 @@
<w>pbkdf</w> <w>pbkdf</w>
<w>pids</w> <w>pids</w>
<w>pkcs</w> <w>pkcs</w>
<w>previewable</w>
<w>previewables</w>
<w>riotx</w> <w>riotx</w>
<w>signin</w> <w>signin</w>
<w>signout</w> <w>signout</w>

View File

@ -1,17 +1,14 @@
Changes in Element 1.0.12 (2020-XX-XX) Changes in Element 1.0.14 (2020-XX-XX)
=================================================== ===================================================
Features ✨: Features ✨:
- Add room aliases management, and room directory visibility management in a dedicated screen (#1579, #2428) - Enable url previews for notices (#2562)
- Room setting: update join rules and guest access (#2442)
Improvements 🙌: Improvements 🙌:
- Add Setting Item to Change PIN (#2462) -
- Improve room history visibility setting UX (#1579)
Bugfix 🐛: Bugfix 🐛:
- Double bottomsheet effect after verify with passphrase - Url previews sometimes attached to wrong message (#2561)
- EditText cursor jumps to the start while typing fast (#2469)
Translations 🗣: Translations 🗣:
- -
@ -20,14 +17,60 @@ SDK API changes ⚠️:
- -
Build 🧱: Build 🧱:
- Upgrade some dependencies and Kotlin version -
- Use fragment-ktx and preference-ktx dependencies (fix lint issue KtxExtensionAvailable)
Test: Test:
- -
Other changes:
- Migrate to ViewBindings (#1072)
Changes in Element 1.0.13 (2020-12-18)
===================================================
Bugfix 🐛:
- Fix MSC2858 implementation details (#2540)
Changes in Element 1.0.12 (2020-12-15)
===================================================
Features ✨:
- Add room aliases management, and room directory visibility management in a dedicated screen (#1579, #2428)
- Room setting: update join rules and guest access (#2442)
- Url preview (#481)
- Store encrypted file in cache and cleanup decrypted file at each app start (#2512)
- Emoji Keyboard (#2520)
- Social login (#2452)
- Support for chat effects in timeline (confetti, snow) (#2535)
Improvements 🙌:
- Add Setting Item to Change PIN (#2462)
- Improve room history visibility setting UX (#1579)
- Matrix.to deeplink custom scheme support
- Homeserver history (#1933)
Bugfix 🐛:
- Fix cancellation of sending event (#2438)
- Double bottomsheet effect after verify with passphrase
- EditText cursor jumps to the start while typing fast (#2469)
- UTD for events before invitation if member state events are hidden (#2486)
- No known servers error is given when joining rooms on new Gitter bridge (#2516)
- Show preview when sending attachment from the keyboard (#2440)
- Do not compress GIFs (#1616, #1254)
SDK API changes ⚠️:
- StateService now exposes suspendable function instead of using MatrixCallback.
- RawCacheStrategy has been moved and renamed to CacheStrategy
- FileService: remove useless FileService.DownloadMode
Build 🧱:
- Upgrade some dependencies and Kotlin version
- Use fragment-ktx and preference-ktx dependencies (fix lint issue KtxExtensionAvailable)
- Upgrade Realm dependency to 10.1.2
Other changes: Other changes:
- Remove "Status.im" theme #2424 - Remove "Status.im" theme #2424
- Log HTTP requests and responses in production (level BASIC, i.e. without any private data)
Changes in Element 1.0.11 (2020-11-27) Changes in Element 1.0.11 (2020-11-27)
=================================================== ===================================================

View File

@ -16,7 +16,6 @@
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
buildscript { buildscript {
repositories { repositories {
@ -55,6 +54,10 @@ android {
kotlinOptions { kotlinOptions {
jvmTarget = '1.8' jvmTarget = '1.8'
} }
buildFeatures {
viewBinding true
}
} }
dependencies { dependencies {

View File

@ -17,19 +17,17 @@
package im.vector.lib.attachmentviewer package im.vector.lib.attachmentviewer
import android.view.View import android.view.View
import android.widget.ImageView import im.vector.lib.attachmentviewer.databinding.ItemAnimatedImageAttachmentBinding
import android.widget.ProgressBar
class AnimatedImageViewHolder constructor(itemView: View) : class AnimatedImageViewHolder constructor(itemView: View) :
BaseViewHolder(itemView) { BaseViewHolder(itemView) {
val touchImageView: ImageView = itemView.findViewById(R.id.imageView) val views = ItemAnimatedImageAttachmentBinding.bind(itemView)
val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress)
internal val target = DefaultImageLoaderTarget(this, this.touchImageView) internal val target = DefaultImageLoaderTarget(this, views.imageView)
override fun onRecycled() { override fun onRecycled() {
super.onRecycled() super.onRecycled()
touchImageView.setImageDrawable(null) views.imageView.setImageDrawable(null)
} }
} }

View File

@ -33,7 +33,8 @@ import androidx.core.view.isVisible
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import kotlinx.android.synthetic.main.activity_attachment_viewer.* import im.vector.lib.attachmentviewer.databinding.ActivityAttachmentViewerBinding
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import kotlin.math.abs import kotlin.math.abs
@ -50,12 +51,14 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
private var overlayView: View? = null private var overlayView: View? = null
set(value) { set(value) {
if (value == overlayView) return if (value == overlayView) return
overlayView?.let { rootContainer.removeView(it) } overlayView?.let { views.rootContainer.removeView(it) }
rootContainer.addView(value) views.rootContainer.addView(value)
value?.updatePadding(top = topInset, bottom = bottomInset) value?.updatePadding(top = topInset, bottom = bottomInset)
field = value field = value
} }
private lateinit var views: ActivityAttachmentViewerBinding
private lateinit var swipeDismissHandler: SwipeToDismissHandler private lateinit var swipeDismissHandler: SwipeToDismissHandler
private lateinit var directionDetector: SwipeDirectionDetector private lateinit var directionDetector: SwipeDirectionDetector
private lateinit var scaleDetector: ScaleGestureDetector private lateinit var scaleDetector: ScaleGestureDetector
@ -95,17 +98,17 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
setContentView(R.layout.activity_attachment_viewer) views = ActivityAttachmentViewerBinding.inflate(layoutInflater)
attachmentPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL setContentView(views.root)
views.attachmentPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL
attachmentsAdapter = AttachmentsAdapter() attachmentsAdapter = AttachmentsAdapter()
attachmentPager.adapter = attachmentsAdapter views.attachmentPager.adapter = attachmentsAdapter
imageTransitionView = transitionImageView imageTransitionView = views.transitionImageView
transitionImageContainer = findViewById(R.id.transitionImageContainer) pager2 = views.attachmentPager
pager2 = attachmentPager
directionDetector = createSwipeDirectionDetector() directionDetector = createSwipeDirectionDetector()
gestureDetector = createGestureDetector() gestureDetector = createGestureDetector()
attachmentPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { views.attachmentPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageScrollStateChanged(state: Int) { override fun onPageScrollStateChanged(state: Int) {
isImagePagerIdle = state == ViewPager2.SCROLL_STATE_IDLE isImagePagerIdle = state == ViewPager2.SCROLL_STATE_IDLE
} }
@ -116,12 +119,12 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
}) })
swipeDismissHandler = createSwipeToDismissHandler() swipeDismissHandler = createSwipeToDismissHandler()
rootContainer.setOnTouchListener(swipeDismissHandler) views.rootContainer.setOnTouchListener(swipeDismissHandler)
rootContainer.viewTreeObserver.addOnGlobalLayoutListener { swipeDismissHandler.translationLimit = dismissContainer.height / 4 } views.rootContainer.viewTreeObserver.addOnGlobalLayoutListener { swipeDismissHandler.translationLimit = views.dismissContainer.height / 4 }
scaleDetector = createScaleGestureDetector() scaleDetector = createScaleGestureDetector()
ViewCompat.setOnApplyWindowInsetsListener(rootContainer) { _, insets -> ViewCompat.setOnApplyWindowInsetsListener(views.rootContainer) { _, insets ->
overlayView?.updatePadding(top = insets.systemWindowInsetTop, bottom = insets.systemWindowInsetBottom) overlayView?.updatePadding(top = insets.systemWindowInsetTop, bottom = insets.systemWindowInsetBottom)
topInset = insets.systemWindowInsetTop topInset = insets.systemWindowInsetTop
bottomInset = insets.systemWindowInsetBottom bottomInset = insets.systemWindowInsetBottom
@ -170,7 +173,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
if (swipeDirection == null && (scaleDetector.isInProgress || ev.pointerCount > 1 || wasScaled)) { if (swipeDirection == null && (scaleDetector.isInProgress || ev.pointerCount > 1 || wasScaled)) {
wasScaled = true wasScaled = true
// Log.v("ATTACHEMENTS", "dispatch to pager") // Log.v("ATTACHEMENTS", "dispatch to pager")
return attachmentPager.dispatchTouchEvent(ev) return views.attachmentPager.dispatchTouchEvent(ev)
} }
// Log.v("ATTACHEMENTS", "is current item scaled ${isScaled()}") // Log.v("ATTACHEMENTS", "is current item scaled ${isScaled()}")
@ -196,16 +199,16 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
private fun handleEventActionDown(event: MotionEvent) { private fun handleEventActionDown(event: MotionEvent) {
swipeDirection = null swipeDirection = null
wasScaled = false wasScaled = false
attachmentPager.dispatchTouchEvent(event) views.attachmentPager.dispatchTouchEvent(event)
swipeDismissHandler.onTouch(rootContainer, event) swipeDismissHandler.onTouch(views.rootContainer, event)
isOverlayWasClicked = dispatchOverlayTouch(event) isOverlayWasClicked = dispatchOverlayTouch(event)
} }
private fun handleEventActionUp(event: MotionEvent) { private fun handleEventActionUp(event: MotionEvent) {
// wasDoubleTapped = false // wasDoubleTapped = false
swipeDismissHandler.onTouch(rootContainer, event) swipeDismissHandler.onTouch(views.rootContainer, event)
attachmentPager.dispatchTouchEvent(event) views.attachmentPager.dispatchTouchEvent(event)
isOverlayWasClicked = dispatchOverlayTouch(event) isOverlayWasClicked = dispatchOverlayTouch(event)
} }
@ -220,12 +223,12 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
private fun toggleOverlayViewVisibility() { private fun toggleOverlayViewVisibility() {
if (systemUiVisibility) { if (systemUiVisibility) {
// we hide // we hide
TransitionManager.beginDelayedTransition(rootContainer) TransitionManager.beginDelayedTransition(views.rootContainer)
hideSystemUI() hideSystemUI()
overlayView?.isVisible = false overlayView?.isVisible = false
} else { } else {
// we show // we show
TransitionManager.beginDelayedTransition(rootContainer) TransitionManager.beginDelayedTransition(views.rootContainer)
showSystemUI() showSystemUI()
overlayView?.isVisible = true overlayView?.isVisible = true
} }
@ -238,11 +241,11 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
return when (swipeDirection) { return when (swipeDirection) {
SwipeDirection.Up, SwipeDirection.Down -> { SwipeDirection.Up, SwipeDirection.Down -> {
if (isSwipeToDismissAllowed && !wasScaled && isImagePagerIdle) { if (isSwipeToDismissAllowed && !wasScaled && isImagePagerIdle) {
swipeDismissHandler.onTouch(rootContainer, event) swipeDismissHandler.onTouch(views.rootContainer, event)
} else true } else true
} }
SwipeDirection.Left, SwipeDirection.Right -> { SwipeDirection.Left, SwipeDirection.Right -> {
attachmentPager.dispatchTouchEvent(event) views.attachmentPager.dispatchTouchEvent(event)
} }
else -> true else -> true
} }
@ -250,8 +253,8 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
private fun handleSwipeViewMove(translationY: Float, translationLimit: Int) { private fun handleSwipeViewMove(translationY: Float, translationLimit: Int) {
val alpha = calculateTranslationAlpha(translationY, translationLimit) val alpha = calculateTranslationAlpha(translationY, translationLimit)
backgroundView.alpha = alpha views.backgroundView.alpha = alpha
dismissContainer.alpha = alpha views.dismissContainer.alpha = alpha
overlayView?.alpha = alpha overlayView?.alpha = alpha
} }
@ -265,7 +268,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
private fun createSwipeToDismissHandler() private fun createSwipeToDismissHandler()
: SwipeToDismissHandler = SwipeToDismissHandler( : SwipeToDismissHandler = SwipeToDismissHandler(
swipeView = dismissContainer, swipeView = views.dismissContainer,
shouldAnimateDismiss = { shouldAnimateDismiss() }, shouldAnimateDismiss = { shouldAnimateDismiss() },
onDismiss = { animateClose() }, onDismiss = { animateClose() },
onSwipeViewMove = ::handleSwipeViewMove) onSwipeViewMove = ::handleSwipeViewMove)

View File

@ -98,7 +98,7 @@ class AttachmentsAdapter : RecyclerView.Adapter<BaseViewHolder>() {
fun isScaled(position: Int): Boolean { fun isScaled(position: Int): Boolean {
val holder = recyclerView?.findViewHolderForAdapterPosition(position) val holder = recyclerView?.findViewHolderForAdapterPosition(position)
if (holder is ZoomableImageViewHolder) { if (holder is ZoomableImageViewHolder) {
return holder.touchImageView.attacher.scale > 1f return holder.views.touchImageView.attacher.scale > 1f
} }
return false return false
} }

View File

@ -44,29 +44,29 @@ internal class DefaultImageLoaderTarget(val holder: AnimatedImageViewHolder, pri
override fun onResourceLoading(uid: String, placeholder: Drawable?) { override fun onResourceLoading(uid: String, placeholder: Drawable?) {
if (holder.boundResourceUid != uid) return if (holder.boundResourceUid != uid) return
holder.imageLoaderProgress.isVisible = true holder.views.imageLoaderProgress.isVisible = true
} }
override fun onLoadFailed(uid: String, errorDrawable: Drawable?) { override fun onLoadFailed(uid: String, errorDrawable: Drawable?) {
if (holder.boundResourceUid != uid) return if (holder.boundResourceUid != uid) return
holder.imageLoaderProgress.isVisible = false holder.views.imageLoaderProgress.isVisible = false
holder.touchImageView.setImageDrawable(errorDrawable) holder.views.imageView.setImageDrawable(errorDrawable)
} }
override fun onResourceCleared(uid: String, placeholder: Drawable?) { override fun onResourceCleared(uid: String, placeholder: Drawable?) {
if (holder.boundResourceUid != uid) return if (holder.boundResourceUid != uid) return
holder.touchImageView.setImageDrawable(placeholder) holder.views.imageView.setImageDrawable(placeholder)
} }
override fun onResourceReady(uid: String, resource: Drawable) { override fun onResourceReady(uid: String, resource: Drawable) {
if (holder.boundResourceUid != uid) return if (holder.boundResourceUid != uid) return
holder.imageLoaderProgress.isVisible = false holder.views.imageLoaderProgress.isVisible = false
// Glide mess up the view size :/ // Glide mess up the view size :/
holder.touchImageView.updateLayoutParams { holder.views.imageView.updateLayoutParams {
width = LinearLayout.LayoutParams.MATCH_PARENT width = LinearLayout.LayoutParams.MATCH_PARENT
height = LinearLayout.LayoutParams.MATCH_PARENT height = LinearLayout.LayoutParams.MATCH_PARENT
} }
holder.touchImageView.setImageDrawable(resource) holder.views.imageView.setImageDrawable(resource)
if (resource is Animatable) { if (resource is Animatable) {
resource.start() resource.start()
} }
@ -77,30 +77,30 @@ internal class DefaultImageLoaderTarget(val holder: AnimatedImageViewHolder, pri
override fun onResourceLoading(uid: String, placeholder: Drawable?) { override fun onResourceLoading(uid: String, placeholder: Drawable?) {
if (holder.boundResourceUid != uid) return if (holder.boundResourceUid != uid) return
holder.imageLoaderProgress.isVisible = true holder.views.imageLoaderProgress.isVisible = true
holder.touchImageView.setImageDrawable(placeholder) holder.views.touchImageView.setImageDrawable(placeholder)
} }
override fun onLoadFailed(uid: String, errorDrawable: Drawable?) { override fun onLoadFailed(uid: String, errorDrawable: Drawable?) {
if (holder.boundResourceUid != uid) return if (holder.boundResourceUid != uid) return
holder.imageLoaderProgress.isVisible = false holder.views.imageLoaderProgress.isVisible = false
holder.touchImageView.setImageDrawable(errorDrawable) holder.views.touchImageView.setImageDrawable(errorDrawable)
} }
override fun onResourceCleared(uid: String, placeholder: Drawable?) { override fun onResourceCleared(uid: String, placeholder: Drawable?) {
if (holder.boundResourceUid != uid) return if (holder.boundResourceUid != uid) return
holder.touchImageView.setImageDrawable(placeholder) holder.views.touchImageView.setImageDrawable(placeholder)
} }
override fun onResourceReady(uid: String, resource: Drawable) { override fun onResourceReady(uid: String, resource: Drawable) {
if (holder.boundResourceUid != uid) return if (holder.boundResourceUid != uid) return
holder.imageLoaderProgress.isVisible = false holder.views.imageLoaderProgress.isVisible = false
// Glide mess up the view size :/ // Glide mess up the view size :/
holder.touchImageView.updateLayoutParams { holder.views.touchImageView.updateLayoutParams {
width = LinearLayout.LayoutParams.MATCH_PARENT width = LinearLayout.LayoutParams.MATCH_PARENT
height = LinearLayout.LayoutParams.MATCH_PARENT height = LinearLayout.LayoutParams.MATCH_PARENT
} }
holder.touchImageView.setImageDrawable(resource) holder.views.touchImageView.setImageDrawable(resource)
} }
} }
} }

View File

@ -49,19 +49,19 @@ internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val
override fun onThumbnailResourceCleared(uid: String, placeholder: Drawable?) { override fun onThumbnailResourceCleared(uid: String, placeholder: Drawable?) {
if (holder.boundResourceUid != uid) return if (holder.boundResourceUid != uid) return
holder.thumbnailImage.setImageDrawable(placeholder) holder.views.videoThumbnailImage.setImageDrawable(placeholder)
} }
override fun onThumbnailResourceReady(uid: String, resource: Drawable) { override fun onThumbnailResourceReady(uid: String, resource: Drawable) {
if (holder.boundResourceUid != uid) return if (holder.boundResourceUid != uid) return
holder.thumbnailImage.setImageDrawable(resource) holder.views.videoThumbnailImage.setImageDrawable(resource)
} }
override fun onVideoFileLoading(uid: String) { override fun onVideoFileLoading(uid: String) {
if (holder.boundResourceUid != uid) return if (holder.boundResourceUid != uid) return
holder.thumbnailImage.isVisible = true holder.views.videoThumbnailImage.isVisible = true
holder.loaderProgressBar.isVisible = true holder.views.videoLoaderProgress.isVisible = true
holder.videoView.isVisible = false holder.views.videoView.isVisible = false
} }
override fun onVideoFileLoadFailed(uid: String) { override fun onVideoFileLoadFailed(uid: String) {
@ -82,8 +82,8 @@ internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val
} }
private fun arrangeForVideoReady() { private fun arrangeForVideoReady() {
holder.thumbnailImage.isVisible = false holder.views.videoThumbnailImage.isVisible = false
holder.loaderProgressBar.isVisible = false holder.views.videoLoaderProgress.isVisible = false
holder.videoView.isVisible = true holder.views.videoView.isVisible = true
} }
} }

View File

@ -18,11 +18,8 @@ package im.vector.lib.attachmentviewer
import android.util.Log import android.util.Log
import android.view.View 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 androidx.core.view.isVisible
import im.vector.lib.attachmentviewer.databinding.ItemVideoAttachmentBinding
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
@ -44,13 +41,9 @@ class VideoViewHolder constructor(itemView: View) :
var eventListener: WeakReference<AttachmentEventListener>? = null var eventListener: WeakReference<AttachmentEventListener>? = null
val thumbnailImage: ImageView = itemView.findViewById(R.id.videoThumbnailImage) val views = ItemVideoAttachmentBinding.bind(itemView)
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) internal val target = DefaultVideoLoaderTarget(this, views.videoThumbnailImage)
override fun onRecycled() { override fun onRecycled() {
super.onRecycled() super.onRecycled()
@ -77,12 +70,12 @@ class VideoViewHolder constructor(itemView: View) :
} }
override fun entersBackground() { override fun entersBackground() {
if (videoView.isPlaying) { if (views.videoView.isPlaying) {
progress = videoView.currentPosition progress = views.videoView.currentPosition
progressDisposable?.dispose() progressDisposable?.dispose()
progressDisposable = null progressDisposable = null
videoView.stopPlayback() views.videoView.stopPlayback()
videoView.pause() views.videoView.pause()
} }
} }
@ -92,9 +85,9 @@ class VideoViewHolder constructor(itemView: View) :
override fun onSelected(selected: Boolean) { override fun onSelected(selected: Boolean) {
if (!selected) { if (!selected) {
if (videoView.isPlaying) { if (views.videoView.isPlaying) {
progress = videoView.currentPosition progress = views.videoView.currentPosition
videoView.stopPlayback() views.videoView.stopPlayback()
} else { } else {
progress = 0 progress = 0
} }
@ -109,34 +102,34 @@ class VideoViewHolder constructor(itemView: View) :
} }
private fun startPlaying() { private fun startPlaying() {
thumbnailImage.isVisible = false views.videoThumbnailImage.isVisible = false
loaderProgressBar.isVisible = false views.videoLoaderProgress.isVisible = false
videoView.isVisible = true views.videoView.isVisible = true
videoView.setOnPreparedListener { views.videoView.setOnPreparedListener {
progressDisposable?.dispose() progressDisposable?.dispose()
progressDisposable = Observable.interval(100, TimeUnit.MILLISECONDS) progressDisposable = Observable.interval(100, TimeUnit.MILLISECONDS)
.timeInterval() .timeInterval()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { .subscribe {
val duration = videoView.duration val duration = views.videoView.duration
val progress = videoView.currentPosition val progress = views.videoView.currentPosition
val isPlaying = videoView.isPlaying val isPlaying = views.videoView.isPlaying
// Log.v("FOO", "isPlaying $isPlaying $progress/$duration") // Log.v("FOO", "isPlaying $isPlaying $progress/$duration")
eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration)) eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration))
} }
} }
try { try {
videoView.setVideoPath(mVideoPath) views.videoView.setVideoPath(mVideoPath)
} catch (failure: Throwable) { } catch (failure: Throwable) {
// Couldn't open // Couldn't open
Log.v(VideoViewHolder::class.java.name, "Failed to start video") Log.v(VideoViewHolder::class.java.name, "Failed to start video")
} }
if (!wasPaused) { if (!wasPaused) {
videoView.start() views.videoView.start()
if (progress > 0) { if (progress > 0) {
videoView.seekTo(progress) views.videoView.seekTo(progress)
} }
} }
} }
@ -146,17 +139,17 @@ class VideoViewHolder constructor(itemView: View) :
when (commands) { when (commands) {
AttachmentCommands.StartVideo -> { AttachmentCommands.StartVideo -> {
wasPaused = false wasPaused = false
videoView.start() views.videoView.start()
} }
AttachmentCommands.PauseVideo -> { AttachmentCommands.PauseVideo -> {
wasPaused = true wasPaused = true
videoView.pause() views.videoView.pause()
} }
is AttachmentCommands.SeekTo -> { is AttachmentCommands.SeekTo -> {
val duration = videoView.duration val duration = views.videoView.duration
if (duration > 0) { if (duration > 0) {
val seekDuration = duration * (commands.percentProgress / 100f) val seekDuration = duration * (commands.percentProgress / 100f)
videoView.seekTo(seekDuration.toInt()) views.videoView.seekTo(seekDuration.toInt())
} }
} }
} }

View File

@ -17,31 +17,29 @@
package im.vector.lib.attachmentviewer package im.vector.lib.attachmentviewer
import android.view.View import android.view.View
import android.widget.ProgressBar import im.vector.lib.attachmentviewer.databinding.ItemImageAttachmentBinding
import com.github.chrisbanes.photoview.PhotoView
class ZoomableImageViewHolder constructor(itemView: View) : class ZoomableImageViewHolder constructor(itemView: View) :
BaseViewHolder(itemView) { BaseViewHolder(itemView) {
val touchImageView: PhotoView = itemView.findViewById(R.id.touchImageView) val views = ItemImageAttachmentBinding.bind(itemView)
val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress)
init { init {
touchImageView.setAllowParentInterceptOnEdge(false) views.touchImageView.setAllowParentInterceptOnEdge(false)
touchImageView.setOnScaleChangeListener { scaleFactor, _, _ -> views.touchImageView.setOnScaleChangeListener { scaleFactor, _, _ ->
// Log.v("ATTACHEMENTS", "scaleFactor $scaleFactor") // Log.v("ATTACHEMENTS", "scaleFactor $scaleFactor")
// It's a bit annoying but when you pitch down the scaling // It's a bit annoying but when you pitch down the scaling
// is not exactly one :/ // is not exactly one :/
touchImageView.setAllowParentInterceptOnEdge(scaleFactor <= 1.0008f) views.touchImageView.setAllowParentInterceptOnEdge(scaleFactor <= 1.0008f)
} }
touchImageView.setScale(1.0f, true) views.touchImageView.setScale(1.0f, true)
touchImageView.setAllowParentInterceptOnEdge(true) views.touchImageView.setAllowParentInterceptOnEdge(true)
} }
internal val target = DefaultImageLoaderTarget.ZoomableImageTarget(this, touchImageView) internal val target = DefaultImageLoaderTarget.ZoomableImageTarget(this, views.touchImageView)
override fun onRecycled() { override fun onRecycled() {
super.onRecycled() super.onRecycled()
touchImageView.setImageDrawable(null) views.touchImageView.setImageDrawable(null)
} }
} }

View File

@ -43,6 +43,10 @@ allprojects {
includeGroupByRegex 'com\\.github\\.chrisbanes' includeGroupByRegex 'com\\.github\\.chrisbanes'
// PFLockScreen-Android // PFLockScreen-Android
includeGroupByRegex 'com\\.github\\.vector-im' includeGroupByRegex 'com\\.github\\.vector-im'
//Chat effects
includeGroupByRegex 'com\\.github\\.jetradarmobile'
includeGroupByRegex 'nl\\.dionsegijn'
} }
} }
maven { maven {

View File

@ -165,7 +165,7 @@ In this case, the user can click on "Sign in with SSO" and the native web browse
> https://homeserver.with.sso/_matrix/client/r0/login/sso/redirect?redirectUrl=element%3A%2F%element > https://homeserver.with.sso/_matrix/client/r0/login/sso/redirect?redirectUrl=element%3A%2F%element
The parameter `redirectUrl` is set to `element://element`. The parameter `redirectUrl` is set to `element://connect`.
ChromeCustomTabs are an intermediate way to display a WebPage, between a WebView and using the external browser. More info can be found [here](https://developer.chrome.com/multidevice/android/customtabs) ChromeCustomTabs are an intermediate way to display a WebPage, between a WebView and using the external browser. More info can be found [here](https://developer.chrome.com/multidevice/android/customtabs)
@ -175,7 +175,7 @@ During the process, user may be asked to validate an email by clicking on a link
Once the process is finished, the web page will call the `redirectUrl` with an extra parameter `loginToken` Once the process is finished, the web page will call the `redirectUrl` with an extra parameter `loginToken`
> element://element?loginToken=MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy > element://connect?loginToken=MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy
This navigation is intercepted by Element by the `LoginActivity`, which will then ask the homeserver to convert this `loginToken` to an access token This navigation is intercepted by Element by the `LoginActivity`, which will then ask the homeserver to convert this `loginToken` to an access token

View File

@ -0,0 +1,2 @@
Main changes in this version: URL Preview, new Emoji keyboard, new room settings capabilities, and snow for Christmas!
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.0.12

View File

@ -0,0 +1,2 @@
Main changes in this version: URL Preview, new Emoji keyboard, new room settings capabilities, and snow for Christmas!
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.0.12

View File

@ -0,0 +1,2 @@
Tämä versio sisältää virheenkorjauksia ja muita parannuksia. Viestien lähettäminen on nyt paljon nopeampaa.
Täysi muutosloki: https://github.com/vector-im/element-android/releases/tag/v1.0.10

View File

@ -0,0 +1,30 @@
Element on uudenlainen viestinsovellus, joka:
1. Antaa sinun päättää yksityisyydestäsi.
2. Antaa sinun kommunikoida kenen tahansa kanssa Matrix-verkossa ja jopa sen ulkopuolella siltaamalla sovelluksiin, kuten Slack
3. Suojaa sinua mainonnalta, tietojen keräämiseltä ja suljetuilta alustoilta
4. Suojaa sinut päästä päähän -salauksella sekä ristiin varmentamisella muiden todentamiseksi
Element eroaa täysin muista viestintäsovelluksista, koska se on hajautettu ja avointa lähdekoodia.
Element antaa sinun isännöidä itse - valita isännän - jotta sinulla on yksityisyys ja voit hallita tietojasi sekä keskustelujasi. Se antaa sinulle pääsyn avoimeen verkkoon; joten et ole jumissa Elementin käyttäjissä.
Element pystyy tekemään kaiken tämän, koska se toimii Matrixilla - avoimella, hajautetun viestinnän standardilla.
Element antaa sinulle hallinnan antamalla sinun valita, kuka isännöi keskustelujasi. Element-sovelluksessa voit valita isännän eri tavoin:
1. Hanki ilmainen tili Matrix-kehittäjien ylläpitämällä matrix.org-palvelimella tai valitse tuhansista vapaaehtoisten ylläpitämistä julkisista palvelimista.
2. Isännöi tiliäsi itse suorittamalla palvelinta omalla laitteellasi
3. Luo tili mukautetulla palvelimella yksinkertaisesti tilaamalla Element Matrix Services -palvelu
<b>Miksi valita Element?</b>
<b>OMAT TIEDOT</b>: Sinä päätät, missä tietosi ja viestisi säilytetään. Hallitset sitä itse, eikä jokin MEGAYHTIÖ, joka tutkii tietojasi tai antaa niitä kolmansille osapuolille.
<b>AVOIN KOMMUNIKOINYI JA YHTEISTYÖ</b>: Voit keskustella kaikkien muiden Matrix-verkon käyttäjien kanssa, riippumatta siitä käyttävätkö he Elementiä tai muuta Matrix-sovellusta, ja vaikka he käyttäisivät eri viestijärjestelmiä, kuten Slack, IRC tai XMPP.
<b>ERITTÄIN TURVALLINEN</b>: Vahva päästä päähän -salaus (vain keskustelussa olevat voivat purkaa viestien salauksen), ja ristiin varmentaminen keskustelun osallistujien laitteiden tarkistamiseksi.
<b>TÄYDELLISTÄ VIESTINTÄÄ</b>: Viestit, ääni- ja videopuhelut, tiedostojen jakaminen, näytön jakaminen ja koko joukko integraatioita, botteja ja widgettejä. Rakenna huoneita, yhteisöjä, pidä yhteyttä ja tee asioita.
<b>MISSÄ TAHANSA OLETKIN</b>: Pidä yhteyttä missä tahansa, täysin synkronoidun viestihistorian kautta kaikilla laitteillasi ja verkossa osoitteessa https://app.element.io.

View File

@ -1 +1 @@
Turvallista, hajautettua keskustelua ja VoIP-puheluita. Pidä tietosi turvassa. Turvallista, hajautettua, keskusteluja ja VoIP-puheluita. Pidä tietosi turvassa.

View File

@ -1 +1,2 @@
// DA FARE Questa nuova versione contiene soprattutto correzioni di errori e miglioramenti. L'invio di messaggi ora è molto più veloce.
Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.0.10

View File

@ -1 +1,2 @@
// A FAZER Esta nova versão contém principalmente correções de erros e melhorias. Enviar mensagens agora é muito mais rápido.
Registro de todas as alterações: https://github.com/vector-im/element-android/releases/tag/v1.0.10

View File

@ -1 +1,2 @@
// ATT GÖRA Den här nya versionen innehåller mest buggfixar och förbättringar. Det går nu mycket snabbare att skicka meddelanden.
Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.0.10

View File

@ -0,0 +1,2 @@
Ця версія містить переважно виправлення помилок та деякі покращення. Відправлення повідомлень стало тепер ще швидшим.
Повний перелік змін: https://github.com/vector-im/element-android/releases/tag/v1.0.10

View File

@ -7,7 +7,7 @@ Element — це застосунок для спілкування та спі
Element ґрунтовно відрізняється від інших застосунків для спілкування та співпраці тому що він є децентралізованим та відкритоджерельним. Element ґрунтовно відрізняється від інших застосунків для спілкування та співпраці тому що він є децентралізованим та відкритоджерельним.
Element дозволяє вам розміщувати сервер в себе або обирати будь-якого з надавачів послуг, таким чином забезпечуючи вам конфіденційність і можливість володіти власними даними й бесідами та контролювати їх. Він надає вам доступ до відкритої мережі, тож ви не є обмеженими спілкуванням виключно з користувачами Element. І він є дуже надійним та безпечним. Element дозволяє вам розміщувати сервер в себе або обирати будь-якого з надавачів послуг, таким чином забезпечуючи вам конфіденційність і можливість володіти власними даними й бесідами та контролювати їх. Він надає вам доступ до відкритої мережі, тож ви не є обмеженими спілкуванням виключно з користувачами Element. І він є дуже надійним та безпечним.
Element здатен забезпечити усе це завдяки тому, що він заснований на протоколі Matrix — стандарті для відкритого та децентралізованого спілкування. Element здатен забезпечити усе це завдяки тому, що він заснований на протоколі Matrix — стандарті для відкритого та децентралізованого спілкування.

View File

@ -1 +1,2 @@
// 待辦事項 這個新版本主要包含錯誤修復與改善。傳送訊息更快了。
完整的變更紀錄請見https://github.com/vector-im/element-android/releases/tag/v1.0.10

View File

@ -18,7 +18,7 @@ org.gradle.jvmargs=-Xmx2048m
org.gradle.vfs.watch=true org.gradle.vfs.watch=true
vector.debugPrivateData=false vector.debugPrivateData=false
vector.httpLogLevel=NONE vector.httpLogLevel=BASIC
# Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above # Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above
#vector.debugPrivateData=true #vector.debugPrivateData=true

View File

@ -38,6 +38,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlin_coroutines_version"
// Paging // Paging
implementation "androidx.paging:paging-runtime-ktx:2.1.2" implementation "androidx.paging:paging-runtime-ktx:2.1.2"

View File

@ -17,14 +17,20 @@
package org.matrix.android.sdk.rx package org.matrix.android.sdk.rx
import android.net.Uri import android.net.Uri
import io.reactivex.Completable
import io.reactivex.Observable
import io.reactivex.Single
import kotlinx.coroutines.rx2.rxCompletable
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
import org.matrix.android.sdk.api.session.room.model.GuestAccess
import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
@ -32,11 +38,6 @@ import org.matrix.android.sdk.api.session.room.send.UserDraft
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.api.util.toOptional
import io.reactivex.Completable
import io.reactivex.Observable
import io.reactivex.Single
import org.matrix.android.sdk.api.session.room.model.GuestAccess
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
class RxRoom(private val room: Room) { class RxRoom(private val room: Room) {
@ -121,28 +122,28 @@ class RxRoom(private val room: Room) {
room.invite3pid(threePid, it) room.invite3pid(threePid, it)
} }
fun updateTopic(topic: String): Completable = completableBuilder<Unit> { fun updateTopic(topic: String): Completable = rxCompletable {
room.updateTopic(topic, it) room.updateTopic(topic)
} }
fun updateName(name: String): Completable = completableBuilder<Unit> { fun updateName(name: String): Completable = rxCompletable {
room.updateName(name, it) room.updateName(name)
} }
fun updateHistoryReadability(readability: RoomHistoryVisibility): Completable = completableBuilder<Unit> { fun updateHistoryReadability(readability: RoomHistoryVisibility): Completable = rxCompletable {
room.updateHistoryReadability(readability, it) room.updateHistoryReadability(readability)
} }
fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?): Completable = completableBuilder<Unit> { fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?): Completable = rxCompletable {
room.updateJoinRule(joinRules, guestAccess, it) room.updateJoinRule(joinRules, guestAccess)
} }
fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder<Unit> { fun updateAvatar(avatarUri: Uri, fileName: String): Completable = rxCompletable {
room.updateAvatar(avatarUri, fileName, it) room.updateAvatar(avatarUri, fileName)
} }
fun deleteAvatar(): Completable = completableBuilder<Unit> { fun deleteAvatar(): Completable = rxCompletable {
room.deleteAvatar(it) room.deleteAvatar()
} }
} }

View File

@ -47,6 +47,7 @@ import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
class RxSession(private val session: Session) { class RxSession(private val session: Session) {
@ -139,7 +140,7 @@ class RxSession(private val session: Session) {
} }
fun getRoomIdByAlias(roomAlias: String, fun getRoomIdByAlias(roomAlias: String,
searchOnServer: Boolean): Single<Optional<String>> = singleBuilder { searchOnServer: Boolean): Single<Optional<RoomAliasDescription>> = singleBuilder {
session.getRoomIdByAlias(roomAlias, searchOnServer, it) session.getRoomIdByAlias(roomAlias, searchOnServer, it)
} }

View File

@ -1,7 +1,7 @@
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-parcelize'
apply plugin: 'realm-android' apply plugin: 'realm-android'
buildscript { buildscript {
@ -9,14 +9,10 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath "io.realm:realm-gradle-plugin:10.0.0" classpath "io.realm:realm-gradle-plugin:10.1.2"
} }
} }
androidExtensions {
experimental = true
}
android { android {
compileSdkVersion 29 compileSdkVersion 29
testOptions.unitTests.includeAndroidResources = true testOptions.unitTests.includeAndroidResources = true
@ -63,7 +59,7 @@ android {
release { release {
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false" buildConfigField "boolean", "LOG_PRIVATE_DATA", "false"
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE" buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.BASIC"
} }
} }

View File

@ -25,6 +25,7 @@ import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.common.DaggerTestMatrixComponent import org.matrix.android.sdk.common.DaggerTestMatrixComponent
@ -49,6 +50,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
@Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver @Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
@Inject internal lateinit var olmManager: OlmManager @Inject internal lateinit var olmManager: OlmManager
@Inject internal lateinit var sessionManager: SessionManager @Inject internal lateinit var sessionManager: SessionManager
@Inject internal lateinit var homeServerHistoryService: HomeServerHistoryService
private val uiHandler = Handler(Looper.getMainLooper()) private val uiHandler = Handler(Looper.getMainLooper())
@ -71,6 +73,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
fun rawService() = rawService fun rawService() = rawService
fun homeServerHistoryService() = homeServerHistoryService
fun legacySessionImporter(): LegacySessionImporter { fun legacySessionImporter(): LegacySessionImporter {
return legacySessionImporter return legacySessionImporter
} }

View File

@ -17,13 +17,13 @@
package org.matrix.android.sdk.internal.crypto.encryption package org.matrix.android.sdk.internal.crypto.encryption
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBe
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.MethodSorters import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.NoOpMatrixCallback
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.Room
@ -57,13 +57,14 @@ class EncryptionTest : InstrumentedTest {
@Test @Test
fun test_EncryptionStateEvent() { fun test_EncryptionStateEvent() {
performTest(roomShouldBeEncrypted = true) { room -> performTest(roomShouldBeEncrypted = true) { room ->
// Send an encryption Event as a State Event runBlocking {
room.sendStateEvent( // Send an encryption Event as a State Event
eventType = EventType.STATE_ROOM_ENCRYPTION, room.sendStateEvent(
stateKey = null, eventType = EventType.STATE_ROOM_ENCRYPTION,
body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent(), stateKey = null,
callback = NoOpMatrixCallback() body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent()
) )
}
} }
} }

View File

@ -264,7 +264,7 @@ class KeysBackupTest : InstrumentedTest {
assertNotNull(decryption) assertNotNull(decryption)
// - Check decryptKeyBackupData() returns stg // - Check decryptKeyBackupData() returns stg
val sessionData = keysBackup val sessionData = keysBackup
.decryptKeyBackupData(keyBackupData!!, .decryptKeyBackupData(keyBackupData,
session.olmInboundGroupSession!!.sessionIdentifier(), session.olmInboundGroupSession!!.sessionIdentifier(),
cryptoTestData.roomId, cryptoTestData.roomId,
decryption!!) decryption!!)

View File

@ -111,7 +111,7 @@ class KeysBackupTestHelper(
Assert.assertTrue(keysBackup.isEnabled) Assert.assertTrue(keysBackup.isEnabled)
stateObserver.stopAndCheckStates(null) stateObserver.stopAndCheckStates(null)
return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version!!) return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version)
} }
/** /**

View File

@ -0,0 +1,108 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.media
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
import org.junit.runner.RunWith
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
@RunWith(AndroidJUnit4::class)
internal class UrlsExtractorTest : InstrumentedTest {
private val urlsExtractor = UrlsExtractor()
@Test
fun wrongEventTypeTest() {
createEvent(body = "https://matrix.org")
.copy(type = EventType.STATE_ROOM_GUEST_ACCESS)
.let { urlsExtractor.extract(it) }
.size shouldBeEqualTo 0
}
@Test
fun oneUrlTest() {
createEvent(body = "https://matrix.org")
.let { urlsExtractor.extract(it) }
.let { result ->
result.size shouldBeEqualTo 1
result[0] shouldBeEqualTo "https://matrix.org"
}
}
@Test
fun withoutProtocolTest() {
createEvent(body = "www.matrix.org")
.let { urlsExtractor.extract(it) }
.size shouldBeEqualTo 0
}
@Test
fun oneUrlWithParamTest() {
createEvent(body = "https://matrix.org?foo=bar")
.let { urlsExtractor.extract(it) }
.let { result ->
result.size shouldBeEqualTo 1
result[0] shouldBeEqualTo "https://matrix.org?foo=bar"
}
}
@Test
fun oneUrlWithParamsTest() {
createEvent(body = "https://matrix.org?foo=bar&bar=foo")
.let { urlsExtractor.extract(it) }
.let { result ->
result.size shouldBeEqualTo 1
result[0] shouldBeEqualTo "https://matrix.org?foo=bar&bar=foo"
}
}
@Test
fun oneUrlInlinedTest() {
createEvent(body = "Hello https://matrix.org, how are you?")
.let { urlsExtractor.extract(it) }
.let { result ->
result.size shouldBeEqualTo 1
result[0] shouldBeEqualTo "https://matrix.org"
}
}
@Test
fun twoUrlsTest() {
createEvent(body = "https://matrix.org https://example.org")
.let { urlsExtractor.extract(it) }
.let { result ->
result.size shouldBeEqualTo 2
result[0] shouldBeEqualTo "https://matrix.org"
result[1] shouldBeEqualTo "https://example.org"
}
}
private fun createEvent(body: String): Event = Event(
type = EventType.MESSAGE,
content = MessageTextContent(
msgType = MessageType.MSGTYPE_TEXT,
body = body
).toContent()
)
}

View File

@ -17,7 +17,6 @@
package org.matrix.android.sdk.internal.network.interceptors package org.matrix.android.sdk.internal.network.interceptors
import androidx.annotation.NonNull import androidx.annotation.NonNull
import org.matrix.android.sdk.BuildConfig
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
@ -38,31 +37,28 @@ class FormattedJsonHttpLogger : HttpLoggingInterceptor.Logger {
*/ */
@Synchronized @Synchronized
override fun log(@NonNull message: String) { override fun log(@NonNull message: String) {
// In RELEASE there is no log, but for sure, test again BuildConfig.DEBUG Timber.v(message)
if (BuildConfig.DEBUG) {
Timber.v(message)
if (message.startsWith("{")) { if (message.startsWith("{")) {
// JSON Detected // JSON Detected
try { try {
val o = JSONObject(message) val o = JSONObject(message)
logJson(o.toString(INDENT_SPACE)) logJson(o.toString(INDENT_SPACE))
} catch (e: JSONException) { } catch (e: JSONException) {
// Finally this is not a JSON string... // Finally this is not a JSON string...
Timber.e(e) Timber.e(e)
} }
} else if (message.startsWith("[")) { } else if (message.startsWith("[")) {
// JSON Array detected // JSON Array detected
try { try {
val o = JSONArray(message) val o = JSONArray(message)
logJson(o.toString(INDENT_SPACE)) logJson(o.toString(INDENT_SPACE))
} catch (e: JSONException) { } catch (e: JSONException) {
// Finally not JSON... // Finally not JSON...
Timber.e(e) Timber.e(e)
}
} }
// Else not a json string to log
} }
// Else not a json string to log
} }
private fun logJson(formattedJson: String) { private fun logJson(formattedJson: String) {

View File

@ -23,6 +23,7 @@ import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.SessionManager
@ -47,6 +48,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
@Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver @Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
@Inject internal lateinit var olmManager: OlmManager @Inject internal lateinit var olmManager: OlmManager
@Inject internal lateinit var sessionManager: SessionManager @Inject internal lateinit var sessionManager: SessionManager
@Inject internal lateinit var homeServerHistoryService: HomeServerHistoryService
init { init {
Monarchy.init(context) Monarchy.init(context)
@ -65,6 +67,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
fun rawService() = rawService fun rawService() = rawService
fun homeServerHistoryService() = homeServerHistoryService
fun legacySessionImporter(): LegacySessionImporter { fun legacySessionImporter(): LegacySessionImporter {
return legacySessionImporter return legacySessionImporter
} }

View File

@ -19,7 +19,7 @@ package org.matrix.android.sdk.api.auth
/** /**
* Path to use when the client does not supported any or all login flows * Path to use when the client does not supported any or all login flows
* Ref: https://matrix.org/docs/spec/client_server/latest#login-fallback * Ref: https://matrix.org/docs/spec/client_server/latest#login-fallback
* */ */
const val LOGIN_FALLBACK_PATH = "/_matrix/static/client/login/" const val LOGIN_FALLBACK_PATH = "/_matrix/static/client/login/"
/** /**
@ -33,5 +33,6 @@ const val REGISTER_FALLBACK_PATH = "/_matrix/static/client/register/"
* Ref: https://matrix.org/docs/spec/client_server/latest#sso-client-login * Ref: https://matrix.org/docs/spec/client_server/latest#sso-client-login
*/ */
const val SSO_REDIRECT_PATH = "/_matrix/client/r0/login/sso/redirect" const val SSO_REDIRECT_PATH = "/_matrix/client/r0/login/sso/redirect"
const val MSC2858_SSO_REDIRECT_PATH = "/_matrix/client/unstable/org.matrix.msc2858/login/sso/redirect"
const val SSO_REDIRECT_URL_PARAM = "redirectUrl" const val SSO_REDIRECT_URL_PARAM = "redirectUrl"

View File

@ -0,0 +1,29 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.auth
/**
* A simple service to remember homeservers you already connected to.
*/
interface HomeServerHistoryService {
fun getKnownServersUrls(): List<String>
fun addHomeServerToHistory(url: String)
fun clearHistory()
}

View File

@ -19,6 +19,7 @@ package org.matrix.android.sdk.api.auth.data
sealed class LoginFlowResult { sealed class LoginFlowResult {
data class Success( data class Success(
val supportedLoginTypes: List<String>, val supportedLoginTypes: List<String>,
val ssoIdentityProviders: List<SsoIdentityProvider>?,
val isLoginAndRegistrationSupported: Boolean, val isLoginAndRegistrationSupported: Boolean,
val homeServerUrl: String, val homeServerUrl: String,
val isOutdatedHomeserver: Boolean val isOutdatedHomeserver: Boolean

View File

@ -0,0 +1,52 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.auth.data
import android.os.Parcelable
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import kotlinx.parcelize.Parcelize
@JsonClass(generateAdapter = true)
@Parcelize
data class SsoIdentityProvider(
/**
* The id field would be opaque with the accepted characters matching unreserved URI characters as defined in RFC3986
* - this was chosen to avoid having to encode special characters in the URL. Max length 128.
*/
@Json(name = "id") val id: String,
/**
* The name field should be the human readable string intended for printing by the client.
*/
@Json(name = "name") val name: String?,
/**
* The icon field is the only optional field and should point to an icon representing the IdP.
* If present then it must be an HTTPS URL to an image resource.
* This should be hosted by the homeserver service provider to not leak the client's IP address unnecessarily.
*/
@Json(name = "icon") val iconUrl: String?
) : Parcelable {
companion object {
// Not really defined by the spec, but we may define some ids here
const val ID_GOOGLE = "google"
const val ID_GITHUB = "github"
const val ID_APPLE = "apple"
const val ID_FACEBOOK = "facebook"
const val ID_TWITTER = "twitter"
}
}

View File

@ -14,16 +14,16 @@
* limitations under the License. * limitations under the License.
*/ */
package org.matrix.android.sdk.api.raw package org.matrix.android.sdk.api.cache
sealed class RawCacheStrategy { sealed class CacheStrategy {
// Data is always fetched from the server // Data is always fetched from the server
object NoCache : RawCacheStrategy() object NoCache : CacheStrategy()
// Once data is retrieved, it is stored for the provided amount of time. // Once data is retrieved, it is stored for the provided amount of time.
// In case of error, and if strict is set to false, the cache can be returned if available // In case of error, and if strict is set to false, the cache can be returned if available
data class TtlCache(val validityDurationInMillis: Long, val strict: Boolean) : RawCacheStrategy() data class TtlCache(val validityDurationInMillis: Long, val strict: Boolean) : CacheStrategy()
// Once retrieved, the data is stored in cache and will be always get from the cache // Once retrieved, the data is stored in cache and will be always get from the cache
object InfiniteCache : RawCacheStrategy() object InfiniteCache : CacheStrategy()
} }

View File

@ -16,6 +16,8 @@
package org.matrix.android.sdk.api.raw package org.matrix.android.sdk.api.raw
import org.matrix.android.sdk.api.cache.CacheStrategy
/** /**
* Useful methods to fetch raw data from the server. The access token will not be used to fetched the data * Useful methods to fetch raw data from the server. The access token will not be used to fetched the data
*/ */
@ -23,7 +25,7 @@ interface RawService {
/** /**
* Get a URL, either from cache or from the remote server, depending on the cache strategy * Get a URL, either from cache or from the remote server, depending on the cache strategy
*/ */
suspend fun getUrl(url: String, rawCacheStrategy: RawCacheStrategy): String suspend fun getUrl(url: String, cacheStrategy: CacheStrategy): String
/** /**
* Specific case for the well-known file. Cache validity is 8 hours * Specific case for the well-known file. Cache validity is 8 hours

View File

@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.group.GroupService
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
import org.matrix.android.sdk.api.session.identity.IdentityService import org.matrix.android.sdk.api.session.identity.IdentityService
import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService
import org.matrix.android.sdk.api.session.media.MediaService
import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.profile.ProfileService
import org.matrix.android.sdk.api.session.pushers.PushersService import org.matrix.android.sdk.api.session.pushers.PushersService
@ -181,6 +182,11 @@ interface Session :
*/ */
fun widgetService(): WidgetService fun widgetService(): WidgetService
/**
* Returns the media service associated with the session
*/
fun mediaService(): MediaService
/** /**
* Returns the integration manager service associated with the session * Returns the integration manager service associated with the session
*/ */

View File

@ -20,7 +20,8 @@ import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.util.MimeTypes.normalizeMimeType
@Parcelize @Parcelize
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
@ -45,5 +46,5 @@ data class ContentAttachmentData(
VIDEO VIDEO
} }
fun getSafeMimeType() = if (mimeType == "image/jpg") "image/jpeg" else mimeType fun getSafeMimeType() = mimeType?.normalizeMimeType()
} }

View File

@ -18,8 +18,12 @@ package org.matrix.android.sdk.api.session.file
import android.net.Uri import android.net.Uri
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
import org.matrix.android.sdk.api.session.room.model.message.getFileName
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
import java.io.File import java.io.File
/** /**
@ -27,23 +31,6 @@ import java.io.File
*/ */
interface FileService { interface FileService {
enum class DownloadMode {
/**
* 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 { enum class FileState {
IN_CACHE, IN_CACHE,
DOWNLOADING, DOWNLOADING,
@ -54,34 +41,79 @@ interface FileService {
* Download a file. * Download a file.
* Result will be a decrypted file, stored in the cache folder. url parameter will be used to create unique filename to avoid name collision. * 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( fun downloadFile(fileName: String,
downloadMode: DownloadMode, mimeType: String?,
id: String, url: String?,
fileName: String, elementToDecrypt: ElementToDecrypt?,
mimeType: String?, callback: MatrixCallback<File>): Cancelable
url: String?,
elementToDecrypt: ElementToDecrypt?,
callback: MatrixCallback<File>): Cancelable
fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean fun downloadFile(messageContent: MessageWithAttachmentContent,
callback: MatrixCallback<File>): Cancelable =
downloadFile(
fileName = messageContent.getFileName(),
mimeType = messageContent.mimeType,
url = messageContent.getFileUrl(),
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
callback = callback
)
fun isFileInCache(mxcUrl: String?,
fileName: String,
mimeType: String?,
elementToDecrypt: ElementToDecrypt?
): Boolean
fun isFileInCache(messageContent: MessageWithAttachmentContent) =
isFileInCache(
mxcUrl = messageContent.getFileUrl(),
fileName = messageContent.getFileName(),
mimeType = messageContent.mimeType,
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt())
/** /**
* Use this URI and pass it to intent using flag Intent.FLAG_GRANT_READ_URI_PERMISSION * 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) * (if not other app won't be able to access it)
*/ */
fun getTemporarySharableURI(mxcUrl: String, mimeType: String?): Uri? fun getTemporarySharableURI(mxcUrl: String?,
fileName: String,
mimeType: String?,
elementToDecrypt: ElementToDecrypt?): Uri?
fun getTemporarySharableURI(messageContent: MessageWithAttachmentContent): Uri? =
getTemporarySharableURI(
mxcUrl = messageContent.getFileUrl(),
fileName = messageContent.getFileName(),
mimeType = messageContent.mimeType,
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt()
)
/** /**
* Get information on the given file. * Get information on the given file.
* Mimetype should be the same one as passed to downloadFile (limitation for now) * Mimetype should be the same one as passed to downloadFile (limitation for now)
*/ */
fun fileState(mxcUrl: String, mimeType: String?): FileState fun fileState(mxcUrl: String?,
fileName: String,
mimeType: String?,
elementToDecrypt: ElementToDecrypt?): FileState
fun fileState(messageContent: MessageWithAttachmentContent): FileState =
fileState(
mxcUrl = messageContent.getFileUrl(),
fileName = messageContent.getFileName(),
mimeType = messageContent.mimeType,
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt()
)
/** /**
* Clears all the files downloaded by the service * Clears all the files downloaded by the service, including decrypted files
*/ */
fun clearCache() fun clearCache()
/**
* Clears all the decrypted files by the service
*/
fun clearDecryptedCache()
/** /**
* Get size of cached files * Get size of cached files
*/ */

View File

@ -0,0 +1,50 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.session.media
import org.matrix.android.sdk.api.cache.CacheStrategy
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.util.JsonDict
interface MediaService {
/**
* Extract URLs from an Event.
* @return the list of URLs contains in the body of the Event. It does not mean that URLs in this list have UrlPreview data
*/
fun extractUrls(event: Event): List<String>
/**
* Get Raw Url Preview data from the homeserver. There is no cache management for this request
* @param url The url to get the preview data from
* @param timestamp The optional timestamp
*/
suspend fun getRawPreviewUrl(url: String, timestamp: Long?): JsonDict
/**
* Get Url Preview data from the homeserver, or from cache, depending on the cache strategy
* @param url The url to get the preview data from
* @param timestamp The optional timestamp. Note that this parameter is not taken into account
* if the data is already in cache and the cache strategy allow to use it
* @param cacheStrategy the cache strategy, see the type for more details
*/
suspend fun getPreviewUrl(url: String, timestamp: Long?, cacheStrategy: CacheStrategy): PreviewUrlData
/**
* Clear the cache of all retrieved UrlPreview data
*/
suspend fun clearCache()
}

View File

@ -0,0 +1,51 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.session.media
/**
* Facility data class to get the common field of a PreviewUrl response form the server
*
* Example of return data for the url `https://matrix.org`:
* <pre>
* {
* "matrix:image:size": 112805,
* "og:description": "Matrix is an open standard for interoperable, decentralised, real-time communication",
* "og:image": "mxc://matrix.org/2020-12-03_uFqjagCCTJbaaJxb",
* "og:image:alt": "Matrix is an open standard for interoperable, decentralised, real-time communication",
* "og:image:height": 467,
* "og:image:type": "image/jpeg",
* "og:image:width": 911,
* "og:locale": "en_US",
* "og:site_name": "Matrix.org",
* "og:title": "Matrix.org",
* "og:type": "website",
* "og:url": "https://matrix.org"
* }
* </pre>
*/
data class PreviewUrlData(
// Value of field "og:url". If not provided, this is the value passed in parameter
val url: String,
// Value of field "og:site_name"
val siteName: String?,
// Value of field "og:title"
val title: String?,
// Value of field "og:description"
val description: String?,
// Value of field "og:image"
val mxcUrl: String?
)

View File

@ -25,6 +25,7 @@ interface PermalinkService {
companion object { companion object {
const val MATRIX_TO_URL_BASE = "https://matrix.to/#/" const val MATRIX_TO_URL_BASE = "https://matrix.to/#/"
const val MATRIX_TO_CUSTOM_SCHEME_URL_BASE = "element://"
} }
/** /**

View File

@ -18,12 +18,15 @@ package org.matrix.android.sdk.api.session.room
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.peeking.PeekResult
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
/** /**
* This interface defines methods to get rooms. It's implemented at the session level. * This interface defines methods to get rooms. It's implemented at the session level.
@ -120,7 +123,7 @@ interface RoomService {
*/ */
fun getRoomIdByAlias(roomAlias: String, fun getRoomIdByAlias(roomAlias: String,
searchOnServer: Boolean, searchOnServer: Boolean,
callback: MatrixCallback<Optional<String>>): Cancelable callback: MatrixCallback<Optional<RoomAliasDescription>>): Cancelable
/** /**
* Delete a room alias * Delete a room alias
@ -163,4 +166,16 @@ interface RoomService {
* @return a LiveData of the optional found room member * @return a LiveData of the optional found room member
*/ */
fun getRoomMemberLive(userId: String, roomId: String): LiveData<Optional<RoomMemberSummary>> fun getRoomMemberLive(userId: String, roomId: String): LiveData<Optional<RoomMemberSummary>>
/**
* Get some state events about a room
*/
fun getRoomState(roomId: String, callback: MatrixCallback<List<Event>>)
/**
* Use this if you want to get information from a room that you are not yet in (or invited)
* It might be possible to get some information on this room if it is public or if guest access is allowed
* This call will try to gather some information on this room, but it could fail and get nothing more
*/
fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback<PeekResult>)
} }

View File

@ -20,6 +20,7 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.api.util.MimeTypes
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
@ -54,5 +55,5 @@ data class MessageImageContent(
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageImageInfoContent { ) : MessageImageInfoContent {
override val mimeType: String? override val mimeType: String?
get() = encryptedFileInfo?.mimetype ?: info?.mimeType ?: "image/*" get() = encryptedFileInfo?.mimetype ?: info?.mimeType ?: MimeTypes.Images
} }

View File

@ -33,4 +33,7 @@ object MessageType {
// Add, in local, a fake message type in order to StickerMessage can inherit Message class // Add, in local, a fake message type in order to StickerMessage can inherit Message class
// Because sticker isn't a message type but a event type without msgtype field // Because sticker isn't a message type but a event type without msgtype field
const val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker" const val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker"
const val MSGTYPE_CONFETTI = "nic.custom.confetti"
const val MSGTYPE_SNOW = "nic.custom.snow"
} }

View File

@ -0,0 +1,37 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.session.room.peeking
sealed class PeekResult {
data class Success(
val roomId: String,
val alias: String?,
val name: String?,
val topic: String?,
val avatarUrl: String?,
val numJoinedMembers: Int?,
val viaServers: List<String>
) : PeekResult()
data class PeekingNotAllowed(
val roomId: String,
val alias: String?,
val viaServers: List<String>
) : PeekResult()
object UnknownAlias : PeekResult()
}

View File

@ -18,13 +18,11 @@ package org.matrix.android.sdk.api.session.room.state
import android.net.Uri import android.net.Uri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.session.room.model.GuestAccess
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
@ -33,41 +31,41 @@ interface StateService {
/** /**
* Update the topic of the room * Update the topic of the room
*/ */
fun updateTopic(topic: String, callback: MatrixCallback<Unit>): Cancelable suspend fun updateTopic(topic: String)
/** /**
* Update the name of the room * Update the name of the room
*/ */
fun updateName(name: String, callback: MatrixCallback<Unit>): Cancelable suspend fun updateName(name: String)
/** /**
* Update the canonical alias of the room * Update the canonical alias of the room
* @param alias the canonical alias, or null to reset the canonical alias of this room * @param alias the canonical alias, or null to reset the canonical alias of this room
* @param altAliases the alternative aliases for this room. It should include the canonical alias if any. * @param altAliases the alternative aliases for this room. It should include the canonical alias if any.
*/ */
fun updateCanonicalAlias(alias: String?, altAliases: List<String>, callback: MatrixCallback<Unit>): Cancelable suspend fun updateCanonicalAlias(alias: String?, altAliases: List<String>)
/** /**
* Update the history readability of the room * Update the history readability of the room
*/ */
fun updateHistoryReadability(readability: RoomHistoryVisibility, callback: MatrixCallback<Unit>): Cancelable suspend fun updateHistoryReadability(readability: RoomHistoryVisibility)
/** /**
* Update the join rule and/or the guest access * Update the join rule and/or the guest access
*/ */
fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?, callback: MatrixCallback<Unit>): Cancelable suspend fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?)
/** /**
* Update the avatar of the room * Update the avatar of the room
*/ */
fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback<Unit>): Cancelable suspend fun updateAvatar(avatarUri: Uri, fileName: String)
/** /**
* Delete the avatar of the room * Delete the avatar of the room
*/ */
fun deleteAvatar(callback: MatrixCallback<Unit>): Cancelable suspend fun deleteAvatar()
fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback<Unit>): Cancelable suspend fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict)
fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event? fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event?

View File

@ -0,0 +1,29 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.session.room.timeline
data class EventTypeFilter(
/**
* Allowed event type.
*/
val eventType: String,
/**
* Allowed state key. Set null if you want to allow all events,
* otherwise allowed events will be filtered according to the given stateKey.
*/
val stateKey: String?
)

View File

@ -36,5 +36,5 @@ data class TimelineEventFilters(
/** /**
* If [filterTypes] is true, the list of types allowed by the list. * If [filterTypes] is true, the list of types allowed by the list.
*/ */
val allowedTypes: List<String> = emptyList() val allowedTypes: List<EventTypeFilter> = emptyList()
) )

View File

@ -0,0 +1,38 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.util
import org.matrix.android.sdk.api.extensions.orFalse
// The Android SDK does not provide constant for mime type, add some of them here
object MimeTypes {
const val Any: String = "*/*"
const val OctetStream = "application/octet-stream"
const val Images = "image/*"
const val Png = "image/png"
const val BadJpg = "image/jpg"
const val Jpeg = "image/jpeg"
const val Gif = "image/gif"
fun String?.normalizeMimeType() = if (this == BadJpg) Jpeg else this
fun String?.isMimeTypeImage() = this?.startsWith("image/").orFalse()
fun String?.isMimeTypeVideo() = this?.startsWith("video/").orFalse()
fun String?.isMimeTypeAudio() = this?.startsWith("audio/").orFalse()
}

View File

@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.di.AuthDatabase
import org.matrix.android.sdk.internal.legacy.DefaultLegacySessionImporter import org.matrix.android.sdk.internal.legacy.DefaultLegacySessionImporter
import org.matrix.android.sdk.internal.wellknown.WellknownModule import org.matrix.android.sdk.internal.wellknown.WellknownModule
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import java.io.File import java.io.File
@Module(includes = [WellknownModule::class]) @Module(includes = [WellknownModule::class])
@ -80,4 +81,7 @@ internal abstract class AuthModule {
@Binds @Binds
abstract fun bindDirectLoginTask(task: DefaultDirectLoginTask): DirectLoginTask abstract fun bindDirectLoginTask(task: DefaultDirectLoginTask): DirectLoginTask
@Binds
abstract fun bindHomeServerHistoryService(service: DefaultHomeServerHistoryService): HomeServerHistoryService
} }

View File

@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.LoginFlowResult import org.matrix.android.sdk.api.auth.data.LoginFlowResult
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.login.LoginWizard import org.matrix.android.sdk.api.auth.login.LoginWizard
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
@ -278,6 +279,7 @@ internal class DefaultAuthenticationService @Inject constructor(
} }
return LoginFlowResult.Success( return LoginFlowResult.Success(
loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, loginFlowResponse.flows.orEmpty().mapNotNull { it.type },
loginFlowResponse.flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider,
versions.isLoginAndRegistrationSupportedBySdk(), versions.isLoginAndRegistrationSupportedBySdk(),
homeServerUrl, homeServerUrl,
!versions.isSupportedBySdk() !versions.isSupportedBySdk()

View File

@ -0,0 +1,50 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.auth
import com.zhuinden.monarchy.Monarchy
import io.realm.kotlin.where
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.internal.database.model.KnownServerUrlEntity
import org.matrix.android.sdk.internal.di.GlobalDatabase
import javax.inject.Inject
class DefaultHomeServerHistoryService @Inject constructor(
@GlobalDatabase private val monarchy: Monarchy
) : HomeServerHistoryService {
override fun getKnownServersUrls(): List<String> {
return monarchy.fetchAllMappedSync(
{ realm ->
realm.where<KnownServerUrlEntity>()
},
{ it.url }
)
}
override fun addHomeServerToHistory(url: String) {
monarchy.writeAsync { realm ->
KnownServerUrlEntity(url).let {
realm.insertOrUpdate(it)
}
}
}
override fun clearHistory() {
monarchy.runTransactionSync { it.where<KnownServerUrlEntity>().findAll().deleteAllFromRealm() }
}
}

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.auth.data
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class LoginFlowResponse( internal data class LoginFlowResponse(
@ -34,5 +35,13 @@ internal data class LoginFlow(
* The login type. This is supplied as the type when logging in. * The login type. This is supplied as the type when logging in.
*/ */
@Json(name = "type") @Json(name = "type")
val type: String? val type: String?,
/**
* Augments m.login.sso flow discovery definition to include metadata on the supported IDPs
* the client can show a button for each of the supported providers
* See MSC #2858
*/
@Json(name = "org.matrix.msc2858.identity_providers")
val ssoIdentityProvider: List<SsoIdentityProvider>?
) )

View File

@ -17,7 +17,7 @@
package org.matrix.android.sdk.internal.auth.registration package org.matrix.android.sdk.internal.auth.registration
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
/** /**
* This class represent a localized privacy policy for registration Flow. * This class represent a localized privacy policy for registration Flow.

View File

@ -51,6 +51,18 @@ data class RegistrationFlowResponse(
* The information that the client will need to know in order to use a given type of authentication. * The information that the client will need to know in order to use a given type of authentication.
* For each login stage type presented, that type may be present as a key in this dictionary. * For each login stage type presented, that type may be present as a key in this dictionary.
* For example, the public key of reCAPTCHA stage could be given here. * For example, the public key of reCAPTCHA stage could be given here.
* other example
* "params": {
* "m.login.sso": {
* "identity_providers": [
* {
* "id": "google",
* "name": "Google",
* "icon": "https://..."
* }
* ]
* }
* }
*/ */
@Json(name = "params") @Json(name = "params")
val params: JsonDict? = null val params: JsonDict? = null

View File

@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.crypto.attachments
import android.os.Parcelable import android.os.Parcelable
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
fun EncryptedFileInfo.toElementToDecrypt(): ElementToDecrypt? { fun EncryptedFileInfo.toElementToDecrypt(): ElementToDecrypt? {
// Check the validity of some fields // Check the validity of some fields

View File

@ -20,6 +20,7 @@ import io.realm.DynamicRealm
import io.realm.RealmMigration import io.realm.RealmMigration
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -27,7 +28,7 @@ import javax.inject.Inject
class RealmSessionStoreMigration @Inject constructor() : RealmMigration { class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
companion object { companion object {
const val SESSION_STORE_SCHEMA_VERSION = 5L const val SESSION_STORE_SCHEMA_VERSION = 6L
} }
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
@ -38,6 +39,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
if (oldVersion <= 2) migrateTo3(realm) if (oldVersion <= 2) migrateTo3(realm)
if (oldVersion <= 3) migrateTo4(realm) if (oldVersion <= 3) migrateTo4(realm)
if (oldVersion <= 4) migrateTo5(realm) if (oldVersion <= 4) migrateTo5(realm)
if (oldVersion <= 5) migrateTo6(realm)
} }
private fun migrateTo1(realm: DynamicRealm) { private fun migrateTo1(realm: DynamicRealm) {
@ -89,4 +91,18 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
?.removeField("adminE2EByDefault") ?.removeField("adminE2EByDefault")
?.removeField("preferredJitsiDomain") ?.removeField("preferredJitsiDomain")
} }
private fun migrateTo6(realm: DynamicRealm) {
Timber.d("Step 5 -> 6")
realm.schema.create("PreviewUrlCacheEntity")
.addField(PreviewUrlCacheEntityFields.URL, String::class.java)
.setRequired(PreviewUrlCacheEntityFields.URL, true)
.addPrimaryKey(PreviewUrlCacheEntityFields.URL)
.addField(PreviewUrlCacheEntityFields.URL_FROM_SERVER, String::class.java)
.addField(PreviewUrlCacheEntityFields.SITE_NAME, String::class.java)
.addField(PreviewUrlCacheEntityFields.TITLE, String::class.java)
.addField(PreviewUrlCacheEntityFields.DESCRIPTION, String::class.java)
.addField(PreviewUrlCacheEntityFields.MXC_URL, String::class.java)
.addField(PreviewUrlCacheEntityFields.LAST_UPDATED_TIMESTAMP, Long::class.java)
}
} }

View File

@ -0,0 +1,27 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.database.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
internal open class KnownServerUrlEntity(
@PrimaryKey
var url: String = ""
) : RealmObject() {
companion object
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.database.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
internal open class PreviewUrlCacheEntity(
@PrimaryKey
var url: String = "",
var urlFromServer: String? = null,
var siteName: String? = null,
var title: String? = null,
var description: String? = null,
var mxcUrl: String? = null,
var lastUpdatedTimestamp: Long = 0L
) : RealmObject() {
companion object
}

View File

@ -48,6 +48,7 @@ import io.realm.annotations.RealmModule
PushRulesEntity::class, PushRulesEntity::class,
PushRuleEntity::class, PushRuleEntity::class,
PushConditionEntity::class, PushConditionEntity::class,
PreviewUrlCacheEntity::class,
PusherEntity::class, PusherEntity::class,
PusherDataEntity::class, PusherDataEntity::class,
ReadReceiptsSummaryEntity::class, ReadReceiptsSummaryEntity::class,

View File

@ -0,0 +1,39 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.database.query
import io.realm.Realm
import io.realm.kotlin.createObject
import io.realm.kotlin.where
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields
/**
* Get the current PreviewUrlCacheEntity, return null if it does not exist
*/
internal fun PreviewUrlCacheEntity.Companion.get(realm: Realm, url: String): PreviewUrlCacheEntity? {
return realm.where<PreviewUrlCacheEntity>()
.equalTo(PreviewUrlCacheEntityFields.URL, url)
.findFirst()
}
/**
* Get the current PreviewUrlCacheEntity, create one if it does not exist
*/
internal fun PreviewUrlCacheEntity.Companion.getOrCreate(realm: Realm, url: String): PreviewUrlCacheEntity {
return get(realm, url) ?: realm.createObject(url)
}

View File

@ -71,8 +71,23 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm,
} }
internal fun RealmQuery<TimelineEventEntity>.filterEvents(filters: TimelineEventFilters): RealmQuery<TimelineEventEntity> { internal fun RealmQuery<TimelineEventEntity>.filterEvents(filters: TimelineEventFilters): RealmQuery<TimelineEventEntity> {
if (filters.filterTypes) { if (filters.filterTypes && filters.allowedTypes.isNotEmpty()) {
`in`(TimelineEventEntityFields.ROOT.TYPE, filters.allowedTypes.toTypedArray()) beginGroup()
filters.allowedTypes.forEachIndexed { index, filter ->
if (filter.stateKey == null) {
equalTo(TimelineEventEntityFields.ROOT.TYPE, filter.eventType)
} else {
beginGroup()
equalTo(TimelineEventEntityFields.ROOT.TYPE, filter.eventType)
and()
equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, filter.stateKey)
endGroup()
}
if (index != filters.allowedTypes.size - 1) {
or()
}
}
endGroup()
} }
if (filters.filterUseless) { if (filters.filterUseless) {
not() not()

View File

@ -25,6 +25,7 @@ import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.auth.AuthModule import org.matrix.android.sdk.internal.auth.AuthModule
@ -62,6 +63,8 @@ internal interface MatrixComponent {
fun rawService(): RawService fun rawService(): RawService
fun homeServerHistoryService(): HomeServerHistoryService
fun context(): Context fun context(): Context
fun matrixConfiguration(): MatrixConfiguration fun matrixConfiguration(): MatrixConfiguration
@ -71,9 +74,6 @@ internal interface MatrixComponent {
@CacheDirectory @CacheDirectory
fun cacheDir(): File fun cacheDir(): File
@ExternalFilesDirectory
fun externalFilesDir(): File?
fun olmManager(): OlmManager fun olmManager(): OlmManager
fun taskExecutor(): TaskExecutor fun taskExecutor(): TaskExecutor

View File

@ -57,13 +57,6 @@ internal object MatrixModule {
return context.cacheDir return context.cacheDir
} }
@JvmStatic
@Provides
@ExternalFilesDirectory
fun providesExternalFilesDir(context: Context): File? {
return context.getExternalFilesDir(null)
}
@JvmStatic @JvmStatic
@Provides @Provides
@MatrixScope @MatrixScope

View File

@ -16,14 +16,15 @@
package org.matrix.android.sdk.internal.network package org.matrix.android.sdk.internal.network
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.internal.network.ssl.CertUtil
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.internal.network.ssl.CertUtil
import retrofit2.Call import retrofit2.Call
import retrofit2.awaitResponse import retrofit2.awaitResponse
import timber.log.Timber
import java.io.IOException import java.io.IOException
internal suspend inline fun <DATA : Any> executeRequest(eventBus: EventBus?, internal suspend inline fun <DATA : Any> executeRequest(eventBus: EventBus?,
@ -49,6 +50,9 @@ internal class Request<DATA : Any>(private val eventBus: EventBus?) {
throw response.toFailure(eventBus) throw response.toFailure(eventBus)
} }
} catch (exception: Throwable) { } catch (exception: Throwable) {
// Log some details about the request which has failed
Timber.e("Exception when executing request ${apiCall.request().method} ${apiCall.request().url.toString().substringBefore("?")}")
// Check if this is a certificateException // Check if this is a certificateException
CertUtil.getCertificateException(exception) CertUtil.getCertificateException(exception)
// TODO Support certificate error once logged // TODO Support certificate error once logged

View File

@ -16,7 +16,7 @@
package org.matrix.android.sdk.internal.raw package org.matrix.android.sdk.internal.raw
import org.matrix.android.sdk.api.raw.RawCacheStrategy import org.matrix.android.sdk.api.cache.CacheStrategy
import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.raw.RawService
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@ -25,15 +25,15 @@ internal class DefaultRawService @Inject constructor(
private val getUrlTask: GetUrlTask, private val getUrlTask: GetUrlTask,
private val cleanRawCacheTask: CleanRawCacheTask private val cleanRawCacheTask: CleanRawCacheTask
) : RawService { ) : RawService {
override suspend fun getUrl(url: String, rawCacheStrategy: RawCacheStrategy): String { override suspend fun getUrl(url: String, cacheStrategy: CacheStrategy): String {
return getUrlTask.execute(GetUrlTask.Params(url, rawCacheStrategy)) return getUrlTask.execute(GetUrlTask.Params(url, cacheStrategy))
} }
override suspend fun getWellknown(userId: String): String { override suspend fun getWellknown(userId: String): String {
val homeServerDomain = userId.substringAfter(":") val homeServerDomain = userId.substringAfter(":")
return getUrl( return getUrl(
"https://$homeServerDomain/.well-known/matrix/client", "https://$homeServerDomain/.well-known/matrix/client",
RawCacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false) CacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false)
) )
} }

View File

@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.raw
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import okhttp3.ResponseBody import okhttp3.ResponseBody
import org.matrix.android.sdk.api.raw.RawCacheStrategy import org.matrix.android.sdk.api.cache.CacheStrategy
import org.matrix.android.sdk.internal.database.model.RawCacheEntity import org.matrix.android.sdk.internal.database.model.RawCacheEntity
import org.matrix.android.sdk.internal.database.query.get import org.matrix.android.sdk.internal.database.query.get
import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrCreate
@ -32,7 +32,7 @@ import javax.inject.Inject
internal interface GetUrlTask : Task<GetUrlTask.Params, String> { internal interface GetUrlTask : Task<GetUrlTask.Params, String> {
data class Params( data class Params(
val url: String, val url: String,
val rawCacheStrategy: RawCacheStrategy val cacheStrategy: CacheStrategy
) )
} }
@ -42,14 +42,14 @@ internal class DefaultGetUrlTask @Inject constructor(
) : GetUrlTask { ) : GetUrlTask {
override suspend fun execute(params: GetUrlTask.Params): String { override suspend fun execute(params: GetUrlTask.Params): String {
return when (params.rawCacheStrategy) { return when (params.cacheStrategy) {
RawCacheStrategy.NoCache -> doRequest(params.url) CacheStrategy.NoCache -> doRequest(params.url)
is RawCacheStrategy.TtlCache -> doRequestWithCache( is CacheStrategy.TtlCache -> doRequestWithCache(
params.url, params.url,
params.rawCacheStrategy.validityDurationInMillis, params.cacheStrategy.validityDurationInMillis,
params.rawCacheStrategy.strict params.cacheStrategy.strict
) )
RawCacheStrategy.InfiniteCache -> doRequestWithCache( CacheStrategy.InfiniteCache -> doRequestWithCache(
params.url, params.url,
Long.MAX_VALUE, Long.MAX_VALUE,
true true

View File

@ -0,0 +1,41 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.raw
import io.realm.DynamicRealm
import io.realm.RealmMigration
import org.matrix.android.sdk.internal.database.model.KnownServerUrlEntityFields
import timber.log.Timber
internal object GlobalRealmMigration : RealmMigration {
// Current schema version
const val SCHEMA_VERSION = 1L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
if (oldVersion <= 0) migrateTo1(realm)
}
private fun migrateTo1(realm: DynamicRealm) {
realm.schema.create("KnownServerUrlEntity")
.addField(KnownServerUrlEntityFields.URL, String::class.java)
.addPrimaryKey(KnownServerUrlEntityFields.URL)
.setRequired(KnownServerUrlEntityFields.URL, true)
}
}

View File

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.raw package org.matrix.android.sdk.internal.raw
import io.realm.annotations.RealmModule import io.realm.annotations.RealmModule
import org.matrix.android.sdk.internal.database.model.KnownServerUrlEntity
import org.matrix.android.sdk.internal.database.model.RawCacheEntity import org.matrix.android.sdk.internal.database.model.RawCacheEntity
/** /**
@ -24,6 +25,7 @@ import org.matrix.android.sdk.internal.database.model.RawCacheEntity
*/ */
@RealmModule(library = true, @RealmModule(library = true,
classes = [ classes = [
RawCacheEntity::class RawCacheEntity::class,
KnownServerUrlEntity::class
]) ])
internal class GlobalRealmModule internal class GlobalRealmModule

View File

@ -57,6 +57,9 @@ internal abstract class RawModule {
realmKeysUtils.configureEncryption(this, DB_ALIAS) realmKeysUtils.configureEncryption(this, DB_ALIAS)
} }
.name("matrix-sdk-global.realm") .name("matrix-sdk-global.realm")
.schemaVersion(GlobalRealmMigration.SCHEMA_VERSION)
.migration(GlobalRealmMigration)
.allowWritesOnUiThread(true)
.modules(GlobalRealmModule()) .modules(GlobalRealmModule())
.build() .build()
} }

View File

@ -21,6 +21,10 @@ import android.net.Uri
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import arrow.core.Try import arrow.core.Try
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.content.ContentUrlResolver
@ -29,35 +33,21 @@ import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.NoOpCancellable import org.matrix.android.sdk.api.util.NoOpCancellable
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
import org.matrix.android.sdk.internal.di.CacheDirectory
import org.matrix.android.sdk.internal.di.ExternalFilesDirectory
import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory
import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress
import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import org.matrix.android.sdk.internal.util.md5
import org.matrix.android.sdk.internal.util.toCancelable import org.matrix.android.sdk.internal.util.toCancelable
import org.matrix.android.sdk.internal.util.writeToFile import org.matrix.android.sdk.internal.util.writeToFile
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okio.buffer
import okio.sink
import okio.source
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream
import java.net.URLEncoder
import javax.inject.Inject import javax.inject.Inject
internal class DefaultFileService @Inject constructor( internal class DefaultFileService @Inject constructor(
private val context: Context, private val context: Context,
@CacheDirectory
private val cacheDirectory: File,
@ExternalFilesDirectory
private val externalFilesDirectory: File?,
@SessionDownloadsDirectory @SessionDownloadsDirectory
private val sessionCacheDirectory: File, private val sessionCacheDirectory: File,
private val contentUrlResolver: ContentUrlResolver, private val contentUrlResolver: ContentUrlResolver,
@ -67,9 +57,17 @@ internal class DefaultFileService @Inject constructor(
private val taskExecutor: TaskExecutor private val taskExecutor: TaskExecutor
) : FileService { ) : FileService {
private fun String.safeFileName() = URLEncoder.encode(this, Charsets.US_ASCII.displayName()) // Legacy folder, will be deleted
private val legacyFolder = File(sessionCacheDirectory, "MF")
// Folder to store downloaded files (not decrypted)
private val downloadFolder = File(sessionCacheDirectory, "F")
// Folder to store decrypted files
private val decryptedFolder = File(downloadFolder, "D")
private val downloadFolder = File(sessionCacheDirectory, "MF") init {
// Clear the legacy downloaded files
legacyFolder.deleteRecursively()
}
/** /**
* Retain ongoing downloads to avoid re-downloading and already downloading file * Retain ongoing downloads to avoid re-downloading and already downloading file
@ -81,28 +79,26 @@ internal class DefaultFileService @Inject constructor(
* Download file in the cache folder, and eventually decrypt it * Download file in the cache folder, and eventually decrypt it
* TODO looks like files are copied 3 times * TODO looks like files are copied 3 times
*/ */
override fun downloadFile(downloadMode: FileService.DownloadMode, override fun downloadFile(fileName: String,
id: String,
fileName: String,
mimeType: String?, mimeType: String?,
url: String?, url: String?,
elementToDecrypt: ElementToDecrypt?, elementToDecrypt: ElementToDecrypt?,
callback: MatrixCallback<File>): Cancelable { callback: MatrixCallback<File>): Cancelable {
val unwrappedUrl = url ?: return NoOpCancellable.also { url ?: return NoOpCancellable.also {
callback.onFailure(IllegalArgumentException("url is null")) callback.onFailure(IllegalArgumentException("url is null"))
} }
Timber.v("## FileService downloadFile $unwrappedUrl") Timber.v("## FileService downloadFile $url")
synchronized(ongoing) { synchronized(ongoing) {
val existing = ongoing[unwrappedUrl] val existing = ongoing[url]
if (existing != null) { if (existing != null) {
Timber.v("## FileService downloadFile is already downloading.. ") Timber.v("## FileService downloadFile is already downloading.. ")
existing.add(callback) existing.add(callback)
return NoOpCancellable return NoOpCancellable
} else { } else {
// mark as tracked // mark as tracked
ongoing[unwrappedUrl] = ArrayList() ongoing[url] = ArrayList()
// and proceed to download // and proceed to download
} }
} }
@ -110,15 +106,15 @@ internal class DefaultFileService @Inject constructor(
return taskExecutor.executorScope.launch(coroutineDispatchers.main) { return taskExecutor.executorScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.io) { withContext(coroutineDispatchers.io) {
Try { Try {
if (!downloadFolder.exists()) { if (!decryptedFolder.exists()) {
downloadFolder.mkdirs() decryptedFolder.mkdirs()
} }
// ensure we use unique file name by using URL (mapped to suitable file name) // ensure we use unique file name by using URL (mapped to suitable file name)
// Also we need to add extension for the FileProvider, if not it lot's of app that it's // Also we need to add extension for the FileProvider, if not it lot's of app that it's
// shared with will not function well (even if mime type is passed in the intent) // shared with will not function well (even if mime type is passed in the intent)
File(downloadFolder, fileForUrl(unwrappedUrl, mimeType)) getFiles(url, fileName, mimeType, elementToDecrypt != null)
}.flatMap { destFile -> }.flatMap { cachedFiles ->
if (!destFile.exists()) { if (!cachedFiles.file.exists()) {
val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null")) val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null"))
val request = Request.Builder() val request = Request.Builder()
@ -141,79 +137,153 @@ internal class DefaultFileService @Inject constructor(
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}") Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}")
if (elementToDecrypt != null) { // Write the file to cache (encrypted version if the file is encrypted)
Timber.v("## FileService: decrypt file") writeToFile(source.inputStream(), cachedFiles.file)
val decryptSuccess = destFile.outputStream().buffered().use { response.close()
MXEncryptedAttachments.decryptAttachment(
source.inputStream(),
elementToDecrypt,
it
)
}
response.close()
if (!decryptSuccess) {
return@flatMap Try.Failure(IllegalStateException("Decryption error"))
}
} else {
writeToFile(source.inputStream(), destFile)
response.close()
}
} else { } else {
Timber.v("## FileService: cache hit for $url") Timber.v("## FileService: cache hit for $url")
} }
Try.just(copyFile(destFile, downloadMode)) Try.just(cachedFiles)
} }
}.fold({ }.flatMap { cachedFiles ->
callback.onFailure(it) // Decrypt if necessary
// notify concurrent requests if (cachedFiles.decryptedFile != null) {
val toNotify = synchronized(ongoing) { if (!cachedFiles.decryptedFile.exists()) {
ongoing[unwrappedUrl]?.also { Timber.v("## FileService: decrypt file")
ongoing.remove(unwrappedUrl) // Ensure the parent folder exists
cachedFiles.decryptedFile.parentFile?.mkdirs()
val decryptSuccess = cachedFiles.file.inputStream().use { inputStream ->
cachedFiles.decryptedFile.outputStream().buffered().use { outputStream ->
MXEncryptedAttachments.decryptAttachment(
inputStream,
elementToDecrypt,
outputStream
)
}
}
if (!decryptSuccess) {
return@flatMap Try.Failure(IllegalStateException("Decryption error"))
}
} else {
Timber.v("## FileService: cache hit for decrypted file")
} }
Try.just(cachedFiles.decryptedFile)
} else {
// Clear file
Try.just(cachedFiles.file)
} }
toNotify?.forEach { otherCallbacks -> }.fold(
tryOrNull { otherCallbacks.onFailure(it) } { throwable ->
} callback.onFailure(throwable)
}, { file -> // notify concurrent requests
callback.onSuccess(file) val toNotify = synchronized(ongoing) {
// notify concurrent requests ongoing[url]?.also {
val toNotify = synchronized(ongoing) { ongoing.remove(url)
ongoing[unwrappedUrl]?.also { }
ongoing.remove(unwrappedUrl) }
toNotify?.forEach { otherCallbacks ->
tryOrNull { otherCallbacks.onFailure(throwable) }
}
},
{ file ->
callback.onSuccess(file)
// notify concurrent requests
val toNotify = synchronized(ongoing) {
ongoing[url]?.also {
ongoing.remove(url)
}
}
Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ")
toNotify?.forEach { otherCallbacks ->
tryOrNull { otherCallbacks.onSuccess(file) }
}
} }
} )
Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ")
toNotify?.forEach { otherCallbacks ->
tryOrNull { otherCallbacks.onSuccess(file) }
}
})
}.toCancelable() }.toCancelable()
} }
fun storeDataFor(url: String, mimeType: String?, inputStream: InputStream) { fun storeDataFor(mxcUrl: String,
val file = File(downloadFolder, fileForUrl(url, mimeType)) filename: String?,
val source = inputStream.source().buffer() mimeType: String?,
file.sink().buffer().let { sink -> originalFile: File,
source.use { input -> encryptedFile: File?) {
sink.use { output -> val files = getFiles(mxcUrl, filename, mimeType, encryptedFile != null)
output.writeAll(input) if (encryptedFile != null) {
// We switch the two files here, original file it the decrypted file
files.decryptedFile?.let { originalFile.copyTo(it) }
encryptedFile.copyTo(files.file)
} else {
// Just copy the original file
originalFile.copyTo(files.file)
}
}
private fun safeFileName(fileName: String?, mimeType: String?): String {
return buildString {
// filename has to be safe for the Android System
val result = fileName
?.replace("[^a-z A-Z0-9\\\\.\\-]".toRegex(), "_")
?.takeIf { it.isNotEmpty() }
?: DEFAULT_FILENAME
append(result)
// Check that the extension is correct regarding the mimeType
val extensionFromMime = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) }
if (extensionFromMime != null) {
// Compare
val fileExtension = result.substringAfterLast(delimiter = ".", missingDelimiterValue = "")
if (fileExtension.isEmpty() || fileExtension != extensionFromMime) {
// Missing extension, or diff in extension, add the one provided by the mimetype
append(".")
append(extensionFromMime)
} }
} }
} }
} }
private fun fileForUrl(url: String, mimeType: String?): String { override fun isFileInCache(mxcUrl: String?,
val extension = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) } fileName: String,
return if (extension != null) "${url.safeFileName()}.$extension" else url.safeFileName() mimeType: String?,
elementToDecrypt: ElementToDecrypt?): Boolean {
return fileState(mxcUrl, fileName, mimeType, elementToDecrypt) == FileService.FileState.IN_CACHE
} }
override fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean { internal data class CachedFiles(
return File(downloadFolder, fileForUrl(mxcUrl, mimeType)).exists() // This is the downloaded file. Can be clear or encrypted
val file: File,
// This is the decrypted file. Null if the original file is not encrypted
val decryptedFile: File?
) {
fun getClearFile(): File = decryptedFile ?: file
} }
override fun fileState(mxcUrl: String, mimeType: String?): FileService.FileState { private fun getFiles(mxcUrl: String,
if (isFileInCache(mxcUrl, mimeType)) return FileService.FileState.IN_CACHE fileName: String?,
mimeType: String?,
isEncrypted: Boolean): CachedFiles {
val hashFolder = mxcUrl.md5()
val safeFileName = safeFileName(fileName, mimeType)
return if (isEncrypted) {
// Encrypted file
CachedFiles(
File(downloadFolder, "$hashFolder/$ENCRYPTED_FILENAME"),
File(decryptedFolder, "$hashFolder/$safeFileName")
)
} else {
// Clear file
CachedFiles(
File(downloadFolder, "$hashFolder/$safeFileName"),
null
)
}
}
override fun fileState(mxcUrl: String?,
fileName: String,
mimeType: String?,
elementToDecrypt: ElementToDecrypt?): FileService.FileState {
mxcUrl ?: return FileService.FileState.UNKNOWN
if (getFiles(mxcUrl, fileName, mimeType, elementToDecrypt != null).file.exists()) return FileService.FileState.IN_CACHE
val isDownloading = synchronized(ongoing) { val isDownloading = synchronized(ongoing) {
ongoing[mxcUrl] != null ongoing[mxcUrl] != null
} }
@ -224,26 +294,18 @@ internal class DefaultFileService @Inject constructor(
* Use this URI and pass it to intent using flag Intent.FLAG_GRANT_READ_URI_PERMISSION * 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) * (if not other app won't be able to access it)
*/ */
override fun getTemporarySharableURI(mxcUrl: String, mimeType: String?): Uri? { override fun getTemporarySharableURI(mxcUrl: String?,
fileName: String,
mimeType: String?,
elementToDecrypt: ElementToDecrypt?): Uri? {
mxcUrl ?: return null
// this string could be extracted no? // this string could be extracted no?
val authority = "${context.packageName}.mx-sdk.fileprovider" val authority = "${context.packageName}.mx-sdk.fileprovider"
val targetFile = File(downloadFolder, fileForUrl(mxcUrl, mimeType)) val targetFile = getFiles(mxcUrl, fileName, mimeType, elementToDecrypt != null).getClearFile()
if (!targetFile.exists()) return null if (!targetFile.exists()) return null
return FileProvider.getUriForFile(context, authority, targetFile) return FileProvider.getUriForFile(context, authority, targetFile)
} }
private fun copyFile(file: File, downloadMode: FileService.DownloadMode): File {
// TODO some of this seems outdated, will need to be re-worked
return when (downloadMode) {
FileService.DownloadMode.TO_EXPORT ->
file.copyTo(File(externalFilesDirectory, file.name), true)
FileService.DownloadMode.FOR_EXTERNAL_SHARE ->
file.copyTo(File(File(cacheDirectory, "ext_share"), file.name), true)
FileService.DownloadMode.FOR_INTERNAL_USE ->
file
}
}
override fun getCacheSize(): Int { override fun getCacheSize(): Int {
return downloadFolder.walkTopDown() return downloadFolder.walkTopDown()
.onEnter { .onEnter {
@ -256,4 +318,14 @@ internal class DefaultFileService @Inject constructor(
override fun clearCache() { override fun clearCache() {
downloadFolder.deleteRecursively() downloadFolder.deleteRecursively()
} }
override fun clearDecryptedCache() {
decryptedFolder.deleteRecursively()
}
companion object {
private const val ENCRYPTED_FILENAME = "encrypted.bin"
// The extension would be added from the mimetype
private const val DEFAULT_FILENAME = "file"
}
} }

View File

@ -43,6 +43,7 @@ import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.group.GroupService import org.matrix.android.sdk.api.session.group.GroupService
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService
import org.matrix.android.sdk.api.session.media.MediaService
import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.profile.ProfileService
import org.matrix.android.sdk.api.session.pushers.PushersService import org.matrix.android.sdk.api.session.pushers.PushersService
@ -102,6 +103,7 @@ internal class DefaultSession @Inject constructor(
private val permalinkService: Lazy<PermalinkService>, private val permalinkService: Lazy<PermalinkService>,
private val secureStorageService: Lazy<SecureStorageService>, private val secureStorageService: Lazy<SecureStorageService>,
private val profileService: Lazy<ProfileService>, private val profileService: Lazy<ProfileService>,
private val mediaService: Lazy<MediaService>,
private val widgetService: Lazy<WidgetService>, private val widgetService: Lazy<WidgetService>,
private val syncThreadProvider: Provider<SyncThread>, private val syncThreadProvider: Provider<SyncThread>,
private val contentUrlResolver: ContentUrlResolver, private val contentUrlResolver: ContentUrlResolver,
@ -263,6 +265,8 @@ internal class DefaultSession @Inject constructor(
override fun widgetService(): WidgetService = widgetService.get() override fun widgetService(): WidgetService = widgetService.get()
override fun mediaService(): MediaService = mediaService.get()
override fun integrationManagerService() = integrationManagerService override fun integrationManagerService() = integrationManagerService
override fun callSignalingService(): CallSignalingService = callSignalingService.get() override fun callSignalingService(): CallSignalingService = callSignalingService.get()

View File

@ -40,6 +40,7 @@ import org.matrix.android.sdk.internal.session.group.GroupModule
import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesModule import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesModule
import org.matrix.android.sdk.internal.session.identity.IdentityModule import org.matrix.android.sdk.internal.session.identity.IdentityModule
import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerModule import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerModule
import org.matrix.android.sdk.internal.session.media.MediaModule
import org.matrix.android.sdk.internal.session.openid.OpenIdModule import org.matrix.android.sdk.internal.session.openid.OpenIdModule
import org.matrix.android.sdk.internal.session.profile.ProfileModule import org.matrix.android.sdk.internal.session.profile.ProfileModule
import org.matrix.android.sdk.internal.session.pushers.AddHttpPusherWorker import org.matrix.android.sdk.internal.session.pushers.AddHttpPusherWorker
@ -75,6 +76,7 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
GroupModule::class, GroupModule::class,
ContentModule::class, ContentModule::class,
CacheModule::class, CacheModule::class,
MediaModule::class,
CryptoModule::class, CryptoModule::class,
PushersModule::class, PushersModule::class,
OpenIdModule::class, OpenIdModule::class,

View File

@ -50,6 +50,7 @@ import org.matrix.android.sdk.internal.database.EventInsertLiveObserver
import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory
import org.matrix.android.sdk.internal.di.Authenticated import org.matrix.android.sdk.internal.di.Authenticated
import org.matrix.android.sdk.internal.di.CacheDirectory
import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.DeviceId
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory
@ -169,9 +170,9 @@ internal abstract class SessionModule {
@JvmStatic @JvmStatic
@Provides @Provides
@SessionDownloadsDirectory @SessionDownloadsDirectory
fun providesCacheDir(@SessionId sessionId: String, fun providesDownloadsCacheDir(@SessionId sessionId: String,
context: Context): File { @CacheDirectory cacheFile: File): File {
return File(context.cacheDir, "downloads/$sessionId") return File(cacheFile, "downloads/$sessionId")
} }
@JvmStatic @JvmStatic

View File

@ -20,6 +20,9 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ContentUploadResponse( internal data class ContentUploadResponse(
/**
* Required. The MXC URI to the uploaded content.
*/
@Json(name = "content_uri") val contentUri: String @Json(name = "content_uri") val contentUri: String
) )

View File

@ -20,6 +20,7 @@ import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.util.MimeTypes
import timber.log.Timber import timber.log.Timber
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
@ -58,7 +59,7 @@ internal object ThumbnailExtractor {
height = thumbnailHeight, height = thumbnailHeight,
size = thumbnailSize.toLong(), size = thumbnailSize.toLong(),
bytes = outputStream.toByteArray(), bytes = outputStream.toByteArray(),
mimeType = "image/jpeg" mimeType = MimeTypes.Jpeg
) )
thumbnail.recycle() thumbnail.recycle()
outputStream.reset() outputStream.reset()

View File

@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.util.MimeTypes
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.ContentMapper
@ -151,7 +152,10 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
params.attachment.size params.attachment.size
) )
if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) { if (attachment.type == ContentAttachmentData.Type.IMAGE
// Do not compress gif
&& attachment.mimeType != MimeTypes.Gif
&& params.compressBeforeSending) {
fileToUpload = imageCompressor.compress(context, workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE) fileToUpload = imageCompressor.compress(context, workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE)
.also { compressedFile -> .also { compressedFile ->
// Get new Bitmap size // Get new Bitmap size
@ -174,14 +178,15 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
} }
} }
val encryptedFile: File?
val contentUploadResponse = if (params.isEncrypted) { val contentUploadResponse = if (params.isEncrypted) {
Timber.v("## FileService: Encrypt file") Timber.v("## FileService: Encrypt file")
val tmpEncrypted = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) encryptedFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
.also { filesToDelete.add(it) } .also { filesToDelete.add(it) }
uploadedFileEncryptedFileInfo = uploadedFileEncryptedFileInfo =
MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), attachment.getSafeMimeType(), tmpEncrypted) { read, total -> MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), attachment.getSafeMimeType(), encryptedFile) { read, total ->
notifyTracker(params) { notifyTracker(params) {
contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong()) contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong())
} }
@ -190,18 +195,23 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
Timber.v("## FileService: Uploading file") Timber.v("## FileService: Uploading file")
fileUploader fileUploader
.uploadFile(tmpEncrypted, attachment.name, "application/octet-stream", progressListener) .uploadFile(encryptedFile, attachment.name, MimeTypes.OctetStream, progressListener)
} else { } else {
Timber.v("## FileService: Clear file") Timber.v("## FileService: Clear file")
encryptedFile = null
fileUploader fileUploader
.uploadFile(fileToUpload, attachment.name, attachment.getSafeMimeType(), progressListener) .uploadFile(fileToUpload, attachment.name, attachment.getSafeMimeType(), progressListener)
} }
Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}") Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}")
try { try {
context.contentResolver.openInputStream(attachment.queryUri)?.let { fileService.storeDataFor(
fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it) mxcUrl = contentUploadResponse.contentUri,
} filename = params.attachment.name,
mimeType = params.attachment.getSafeMimeType(),
originalFile = workingFile,
encryptedFile = encryptedFile
)
Timber.v("## FileService: cache storage updated") Timber.v("## FileService: cache storage updated")
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.e(failure, "## FileService: Failed to update file cache") Timber.e(failure, "## FileService: Failed to update file cache")
@ -252,7 +262,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
val encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream(), thumbnailData.mimeType) val encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream(), thumbnailData.mimeType)
val contentUploadResponse = fileUploader.uploadByteArray(encryptionResult.encryptedByteArray, val contentUploadResponse = fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
"thumb_${params.attachment.name}", "thumb_${params.attachment.name}",
"application/octet-stream", MimeTypes.OctetStream,
thumbnailProgressListener) thumbnailProgressListener)
UploadThumbnailResult( UploadThumbnailResult(
contentUploadResponse.contentUri, contentUploadResponse.contentUri,

View File

@ -22,19 +22,12 @@ import retrofit2.Call
import retrofit2.http.GET import retrofit2.http.GET
internal interface CapabilitiesAPI { internal interface CapabilitiesAPI {
/** /**
* Request the homeserver capabilities * Request the homeserver capabilities
*/ */
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "capabilities") @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "capabilities")
fun getCapabilities(): Call<GetCapabilitiesResult> fun getCapabilities(): Call<GetCapabilitiesResult>
/**
* Request the upload capabilities
*/
@GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config")
fun getUploadCapabilities(): Call<GetUploadCapabilitiesResult>
/** /**
* Request the versions * Request the versions
*/ */

View File

@ -29,6 +29,8 @@ import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerConfigExtractor import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerConfigExtractor
import org.matrix.android.sdk.internal.session.media.GetMediaConfigResult
import org.matrix.android.sdk.internal.session.media.MediaAPI
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.awaitTransaction
import org.matrix.android.sdk.internal.wellknown.GetWellknownTask import org.matrix.android.sdk.internal.wellknown.GetWellknownTask
@ -40,6 +42,7 @@ internal interface GetHomeServerCapabilitiesTask : Task<Unit, Unit>
internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
private val capabilitiesAPI: CapabilitiesAPI, private val capabilitiesAPI: CapabilitiesAPI,
private val mediaAPI: MediaAPI,
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val eventBus: EventBus, private val eventBus: EventBus,
private val getWellknownTask: GetWellknownTask, private val getWellknownTask: GetWellknownTask,
@ -67,9 +70,9 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
} }
}.getOrNull() }.getOrNull()
val uploadCapabilities = runCatching { val mediaConfig = runCatching {
executeRequest<GetUploadCapabilitiesResult>(eventBus) { executeRequest<GetMediaConfigResult>(eventBus) {
apiCall = capabilitiesAPI.getUploadCapabilities() apiCall = mediaAPI.getMediaConfig()
} }
}.getOrNull() }.getOrNull()
@ -83,11 +86,11 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
getWellknownTask.execute(GetWellknownTask.Params(userId, homeServerConnectionConfig)) getWellknownTask.execute(GetWellknownTask.Params(userId, homeServerConnectionConfig))
}.getOrNull() }.getOrNull()
insertInDb(capabilities, uploadCapabilities, versions, wellknownResult) insertInDb(capabilities, mediaConfig, versions, wellknownResult)
} }
private suspend fun insertInDb(getCapabilitiesResult: GetCapabilitiesResult?, private suspend fun insertInDb(getCapabilitiesResult: GetCapabilitiesResult?,
getUploadCapabilitiesResult: GetUploadCapabilitiesResult?, getMediaConfigResult: GetMediaConfigResult?,
getVersionResult: Versions?, getVersionResult: Versions?,
getWellknownResult: WellknownResult?) { getWellknownResult: WellknownResult?) {
monarchy.awaitTransaction { realm -> monarchy.awaitTransaction { realm ->
@ -97,8 +100,8 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword() homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword()
} }
if (getUploadCapabilitiesResult != null) { if (getMediaConfigResult != null) {
homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize homeServerCapabilitiesEntity.maxUploadFileSize = getMediaConfigResult.maxUploadSize
?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN ?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN
} }

View File

@ -93,7 +93,7 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor(
} catch (failure: Throwable) { } catch (failure: Throwable) {
// Catch invalid hash pepper and retry // Catch invalid hash pepper and retry
if (canRetry && failure is Failure.ServerError && failure.error.code == MatrixError.M_INVALID_PEPPER) { if (canRetry && failure is Failure.ServerError && failure.error.code == MatrixError.M_INVALID_PEPPER) {
// This is not documented, by the error can contain the new pepper! // This is not documented, but the error can contain the new pepper!
if (!failure.error.newLookupPepper.isNullOrEmpty()) { if (!failure.error.newLookupPepper.isNullOrEmpty()) {
// Store it and use it right now // Store it and use it right now
hashDetailResponse.copy(pepper = failure.error.newLookupPepper) hashDetailResponse.copy(pepper = failure.error.newLookupPepper)

View File

@ -0,0 +1,40 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.media
import com.zhuinden.monarchy.Monarchy
import io.realm.kotlin.where
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import javax.inject.Inject
internal interface ClearPreviewUrlCacheTask : Task<Unit, Unit>
internal class DefaultClearPreviewUrlCacheTask @Inject constructor(
@SessionDatabase private val monarchy: Monarchy
) : ClearPreviewUrlCacheTask {
override suspend fun execute(params: Unit) {
monarchy.awaitTransaction { realm ->
realm.where<PreviewUrlCacheEntity>()
.findAll()
.deleteAllFromRealm()
}
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.media
import androidx.collection.LruCache
import org.matrix.android.sdk.api.cache.CacheStrategy
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.media.MediaService
import org.matrix.android.sdk.api.session.media.PreviewUrlData
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.util.getOrPut
import javax.inject.Inject
internal class DefaultMediaService @Inject constructor(
private val clearPreviewUrlCacheTask: ClearPreviewUrlCacheTask,
private val getPreviewUrlTask: GetPreviewUrlTask,
private val getRawPreviewUrlTask: GetRawPreviewUrlTask,
private val urlsExtractor: UrlsExtractor
) : MediaService {
// Cache of extracted URLs
private val extractedUrlsCache = LruCache<String, List<String>>(1_000)
override fun extractUrls(event: Event): List<String> {
return extractedUrlsCache.getOrPut(event.cacheKey()) { urlsExtractor.extract(event) }
}
private fun Event.cacheKey() = "${eventId ?: ""}-${roomId ?: ""}"
override suspend fun getRawPreviewUrl(url: String, timestamp: Long?): JsonDict {
return getRawPreviewUrlTask.execute(GetRawPreviewUrlTask.Params(url, timestamp))
}
override suspend fun getPreviewUrl(url: String, timestamp: Long?, cacheStrategy: CacheStrategy): PreviewUrlData {
return getPreviewUrlTask.execute(GetPreviewUrlTask.Params(url, timestamp, cacheStrategy))
}
override suspend fun clearCache() {
extractedUrlsCache.evictAll()
clearPreviewUrlCacheTask.execute(Unit)
}
}

View File

@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -14,13 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
package org.matrix.android.sdk.internal.session.homeserver package org.matrix.android.sdk.internal.session.media
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class GetUploadCapabilitiesResult( internal data class GetMediaConfigResult(
/** /**
* The maximum size an upload can be in bytes. Clients SHOULD use this as a guide when uploading content. * The maximum size an upload can be in bytes. Clients SHOULD use this as a guide when uploading content.
* If not listed or null, the size limit should be treated as unknown. * If not listed or null, the size limit should be treated as unknown.

View File

@ -0,0 +1,122 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.media
import com.zhuinden.monarchy.Monarchy
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.cache.CacheStrategy
import org.matrix.android.sdk.api.session.media.PreviewUrlData
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity
import org.matrix.android.sdk.internal.database.query.get
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import java.util.Date
import javax.inject.Inject
internal interface GetPreviewUrlTask : Task<GetPreviewUrlTask.Params, PreviewUrlData> {
data class Params(
val url: String,
val timestamp: Long?,
val cacheStrategy: CacheStrategy
)
}
internal class DefaultGetPreviewUrlTask @Inject constructor(
private val mediaAPI: MediaAPI,
private val eventBus: EventBus,
@SessionDatabase private val monarchy: Monarchy
) : GetPreviewUrlTask {
override suspend fun execute(params: GetPreviewUrlTask.Params): PreviewUrlData {
return when (params.cacheStrategy) {
CacheStrategy.NoCache -> doRequest(params.url, params.timestamp)
is CacheStrategy.TtlCache -> doRequestWithCache(
params.url,
params.timestamp,
params.cacheStrategy.validityDurationInMillis,
params.cacheStrategy.strict
)
CacheStrategy.InfiniteCache -> doRequestWithCache(
params.url,
params.timestamp,
Long.MAX_VALUE,
true
)
}
}
private suspend fun doRequest(url: String, timestamp: Long?): PreviewUrlData {
return executeRequest<JsonDict>(eventBus) {
apiCall = mediaAPI.getPreviewUrlData(url, timestamp)
}
.toPreviewUrlData(url)
}
private fun JsonDict.toPreviewUrlData(url: String): PreviewUrlData {
return PreviewUrlData(
url = (get("og:url") as? String) ?: url,
siteName = get("og:site_name") as? String,
title = get("og:title") as? String,
description = get("og:description") as? String,
mxcUrl = get("og:image") as? String
)
}
private suspend fun doRequestWithCache(url: String, timestamp: Long?, validityDurationInMillis: Long, strict: Boolean): PreviewUrlData {
// Get data from cache
var dataFromCache: PreviewUrlData? = null
var isCacheValid = false
monarchy.doWithRealm { realm ->
val entity = PreviewUrlCacheEntity.get(realm, url)
dataFromCache = entity?.toDomain()
isCacheValid = entity != null && Date().time < entity.lastUpdatedTimestamp + validityDurationInMillis
}
val finalDataFromCache = dataFromCache
if (finalDataFromCache != null && isCacheValid) {
return finalDataFromCache
}
// No cache or outdated cache
val data = try {
doRequest(url, timestamp)
} catch (throwable: Throwable) {
// In case of error, we can return value from cache even if outdated
return finalDataFromCache
?.takeIf { !strict }
?: throw throwable
}
// Store cache
monarchy.awaitTransaction { realm ->
val previewUrlCacheEntity = PreviewUrlCacheEntity.getOrCreate(realm, url)
previewUrlCacheEntity.urlFromServer = data.url
previewUrlCacheEntity.siteName = data.siteName
previewUrlCacheEntity.title = data.title
previewUrlCacheEntity.description = data.description
previewUrlCacheEntity.mxcUrl = data.mxcUrl
previewUrlCacheEntity.lastUpdatedTimestamp = Date().time
}
return data
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.media
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface GetRawPreviewUrlTask : Task<GetRawPreviewUrlTask.Params, JsonDict> {
data class Params(
val url: String,
val timestamp: Long?
)
}
internal class DefaultGetRawPreviewUrlTask @Inject constructor(
private val mediaAPI: MediaAPI,
private val eventBus: EventBus
) : GetRawPreviewUrlTask {
override suspend fun execute(params: GetRawPreviewUrlTask.Params): JsonDict {
return executeRequest(eventBus) {
apiCall = mediaAPI.getPreviewUrlData(params.url, params.timestamp)
}
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.media
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
internal interface MediaAPI {
/**
* Retrieve the configuration of the content repository
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-media-r0-config
*/
@GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config")
fun getMediaConfig(): Call<GetMediaConfigResult>
/**
* Get information about a URL for the client. Typically this is called when a client
* sees a URL in a message and wants to render a preview for the user.
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-media-r0-preview-url
* @param url Required. The URL to get a preview of.
* @param ts The preferred point in time to return a preview for. The server may return a newer version
* if it does not have the requested version available.
*/
@GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "preview_url")
fun getPreviewUrlData(@Query("url") url: String, @Query("ts") ts: Long?): Call<JsonDict>
}

View File

@ -0,0 +1,50 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.media
import dagger.Binds
import dagger.Module
import dagger.Provides
import org.matrix.android.sdk.api.session.media.MediaService
import org.matrix.android.sdk.internal.session.SessionScope
import retrofit2.Retrofit
@Module
internal abstract class MediaModule {
@Module
companion object {
@Provides
@JvmStatic
@SessionScope
fun providesMediaAPI(retrofit: Retrofit): MediaAPI {
return retrofit.create(MediaAPI::class.java)
}
}
@Binds
abstract fun bindMediaService(service: DefaultMediaService): MediaService
@Binds
abstract fun bindGetRawPreviewUrlTask(task: DefaultGetRawPreviewUrlTask): GetRawPreviewUrlTask
@Binds
abstract fun bindGetPreviewUrlTask(task: DefaultGetPreviewUrlTask): GetPreviewUrlTask
@Binds
abstract fun bindClearMediaCacheTask(task: DefaultClearPreviewUrlCacheTask): ClearPreviewUrlCacheTask
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.media
import org.matrix.android.sdk.api.session.media.PreviewUrlData
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntity
/**
* PreviewUrlCacheEntity -> PreviewUrlData
*/
internal fun PreviewUrlCacheEntity.toDomain() = PreviewUrlData(
url = urlFromServer ?: url,
siteName = siteName,
title = title,
description = description,
mxcUrl = mxcUrl
)

View File

@ -0,0 +1,48 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.media
import android.util.Patterns
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import javax.inject.Inject
internal class UrlsExtractor @Inject constructor() {
// Sadly Patterns.WEB_URL_WITH_PROTOCOL is not public so filter the protocol later
private val urlRegex = Patterns.WEB_URL.toRegex()
fun extract(event: Event): List<String> {
return event.takeIf { it.getClearType() == EventType.MESSAGE }
?.getClearContent()
?.toModel<MessageContent>()
?.takeIf {
it.msgType == MessageType.MSGTYPE_TEXT
|| it.msgType == MessageType.MSGTYPE_NOTICE
|| it.msgType == MessageType.MSGTYPE_EMOTE
}
?.body
?.let { urlRegex.findAll(it) }
?.map { it.value }
?.filter { it.startsWith("https://") || it.startsWith("http://") }
?.distinct()
?.toList()
.orEmpty()
}
}

View File

@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.profile.ProfileService
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.MimeTypes
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
import org.matrix.android.sdk.internal.database.model.UserThreePidEntity import org.matrix.android.sdk.internal.database.model.UserThreePidEntity
@ -80,7 +81,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
override fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback<Unit>): Cancelable { override fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback<Unit>): Cancelable {
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, matrixCallback) { return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, matrixCallback) {
val response = fileUploader.uploadFromUri(newAvatarUri, fileName, "image/jpeg") val response = fileUploader.uploadFromUri(newAvatarUri, fileName, MimeTypes.Jpeg)
setAvatarUrlTask.execute(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri)) setAvatarUrlTask.execute(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri))
userStore.updateAvatar(userId, response.contentUri) userStore.updateAvatar(userId, response.contentUri)
} }

View File

@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.RoomService import org.matrix.android.sdk.api.session.room.RoomService
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
@ -27,6 +28,7 @@ import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.peeking.PeekResult
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.api.util.toOptional
@ -35,10 +37,13 @@ import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFie
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask
import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask
import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask
import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask
import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask
import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask
@ -55,6 +60,8 @@ internal class DefaultRoomService @Inject constructor(
private val updateBreadcrumbsTask: UpdateBreadcrumbsTask, private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
private val roomIdByAliasTask: GetRoomIdByAliasTask, private val roomIdByAliasTask: GetRoomIdByAliasTask,
private val deleteRoomAliasTask: DeleteRoomAliasTask, private val deleteRoomAliasTask: DeleteRoomAliasTask,
private val resolveRoomStateTask: ResolveRoomStateTask,
private val peekRoomTask: PeekRoomTask,
private val roomGetter: RoomGetter, private val roomGetter: RoomGetter,
private val roomSummaryDataSource: RoomSummaryDataSource, private val roomSummaryDataSource: RoomSummaryDataSource,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
@ -119,7 +126,7 @@ internal class DefaultRoomService @Inject constructor(
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback<Optional<String>>): Cancelable { override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback<Optional<RoomAliasDescription>>): Cancelable {
return roomIdByAliasTask return roomIdByAliasTask
.configureWith(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer)) { .configureWith(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer)) {
this.callback = callback this.callback = callback
@ -154,4 +161,20 @@ internal class DefaultRoomService @Inject constructor(
results.firstOrNull().toOptional() results.firstOrNull().toOptional()
} }
} }
override fun getRoomState(roomId: String, callback: MatrixCallback<List<Event>>) {
resolveRoomStateTask
.configureWith(ResolveRoomStateTask.Params(roomId)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback<PeekResult>) {
peekRoomTask
.configureWith(PeekRoomTask.Params(roomIdOrAlias)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
} }

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