diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index 5ad39614b7..16cc35cebe 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -24,6 +24,8 @@ pbkdf pids pkcs + previewable + previewables riotx signin signout diff --git a/CHANGES.md b/CHANGES.md index b16a6690bc..a028ef6f1e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,17 +1,14 @@ -Changes in Element 1.0.12 (2020-XX-XX) +Changes in Element 1.0.14 (2020-XX-XX) =================================================== 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) + - Enable url previews for notices (#2562) Improvements 🙌: - - Add Setting Item to Change PIN (#2462) - - Improve room history visibility setting UX (#1579) + - Bugfix 🐛: - - Double bottomsheet effect after verify with passphrase - - EditText cursor jumps to the start while typing fast (#2469) + - Url previews sometimes attached to wrong message (#2561) Translations 🗣: - @@ -20,14 +17,60 @@ SDK API changes ⚠️: - Build 🧱: - - Upgrade some dependencies and Kotlin version - - Use fragment-ktx and preference-ktx dependencies (fix lint issue KtxExtensionAvailable) + - 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: - 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) =================================================== diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle index 59ba6c4500..d8cd7d0c98 100644 --- a/attachment-viewer/build.gradle +++ b/attachment-viewer/build.gradle @@ -16,7 +16,6 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' buildscript { repositories { @@ -55,6 +54,10 @@ android { kotlinOptions { jvmTarget = '1.8' } + + buildFeatures { + viewBinding true + } } dependencies { diff --git a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AnimatedImageViewHolder.kt b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AnimatedImageViewHolder.kt index 96e6c92467..0d9eaf59b7 100644 --- a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AnimatedImageViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AnimatedImageViewHolder.kt @@ -17,19 +17,17 @@ package im.vector.lib.attachmentviewer import android.view.View -import android.widget.ImageView -import android.widget.ProgressBar +import im.vector.lib.attachmentviewer.databinding.ItemAnimatedImageAttachmentBinding class AnimatedImageViewHolder constructor(itemView: View) : BaseViewHolder(itemView) { - val touchImageView: ImageView = itemView.findViewById(R.id.imageView) - val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress) + val views = ItemAnimatedImageAttachmentBinding.bind(itemView) - internal val target = DefaultImageLoaderTarget(this, this.touchImageView) + internal val target = DefaultImageLoaderTarget(this, views.imageView) override fun onRecycled() { super.onRecycled() - touchImageView.setImageDrawable(null) + views.imageView.setImageDrawable(null) } } diff --git a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt index 71ce436cf2..ae095be41a 100644 --- a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt +++ b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt @@ -33,7 +33,8 @@ import androidx.core.view.isVisible import androidx.core.view.updatePadding import androidx.transition.TransitionManager import androidx.viewpager2.widget.ViewPager2 -import kotlinx.android.synthetic.main.activity_attachment_viewer.* +import im.vector.lib.attachmentviewer.databinding.ActivityAttachmentViewerBinding + import java.lang.ref.WeakReference import kotlin.math.abs @@ -50,12 +51,14 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi private var overlayView: View? = null set(value) { if (value == overlayView) return - overlayView?.let { rootContainer.removeView(it) } - rootContainer.addView(value) + overlayView?.let { views.rootContainer.removeView(it) } + views.rootContainer.addView(value) value?.updatePadding(top = topInset, bottom = bottomInset) field = value } + private lateinit var views: ActivityAttachmentViewerBinding + private lateinit var swipeDismissHandler: SwipeToDismissHandler private lateinit var directionDetector: SwipeDirectionDetector 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_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) - setContentView(R.layout.activity_attachment_viewer) - attachmentPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL + views = ActivityAttachmentViewerBinding.inflate(layoutInflater) + setContentView(views.root) + views.attachmentPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL attachmentsAdapter = AttachmentsAdapter() - attachmentPager.adapter = attachmentsAdapter - imageTransitionView = transitionImageView - transitionImageContainer = findViewById(R.id.transitionImageContainer) - pager2 = attachmentPager + views.attachmentPager.adapter = attachmentsAdapter + imageTransitionView = views.transitionImageView + pager2 = views.attachmentPager directionDetector = createSwipeDirectionDetector() gestureDetector = createGestureDetector() - attachmentPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + views.attachmentPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageScrollStateChanged(state: Int) { isImagePagerIdle = state == ViewPager2.SCROLL_STATE_IDLE } @@ -116,12 +119,12 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi }) swipeDismissHandler = createSwipeToDismissHandler() - rootContainer.setOnTouchListener(swipeDismissHandler) - rootContainer.viewTreeObserver.addOnGlobalLayoutListener { swipeDismissHandler.translationLimit = dismissContainer.height / 4 } + views.rootContainer.setOnTouchListener(swipeDismissHandler) + views.rootContainer.viewTreeObserver.addOnGlobalLayoutListener { swipeDismissHandler.translationLimit = views.dismissContainer.height / 4 } scaleDetector = createScaleGestureDetector() - ViewCompat.setOnApplyWindowInsetsListener(rootContainer) { _, insets -> + ViewCompat.setOnApplyWindowInsetsListener(views.rootContainer) { _, insets -> overlayView?.updatePadding(top = insets.systemWindowInsetTop, bottom = insets.systemWindowInsetBottom) topInset = insets.systemWindowInsetTop bottomInset = insets.systemWindowInsetBottom @@ -170,7 +173,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi if (swipeDirection == null && (scaleDetector.isInProgress || ev.pointerCount > 1 || wasScaled)) { wasScaled = true // Log.v("ATTACHEMENTS", "dispatch to pager") - return attachmentPager.dispatchTouchEvent(ev) + return views.attachmentPager.dispatchTouchEvent(ev) } // Log.v("ATTACHEMENTS", "is current item scaled ${isScaled()}") @@ -196,16 +199,16 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi private fun handleEventActionDown(event: MotionEvent) { swipeDirection = null wasScaled = false - attachmentPager.dispatchTouchEvent(event) + views.attachmentPager.dispatchTouchEvent(event) - swipeDismissHandler.onTouch(rootContainer, event) + swipeDismissHandler.onTouch(views.rootContainer, event) isOverlayWasClicked = dispatchOverlayTouch(event) } private fun handleEventActionUp(event: MotionEvent) { // wasDoubleTapped = false - swipeDismissHandler.onTouch(rootContainer, event) - attachmentPager.dispatchTouchEvent(event) + swipeDismissHandler.onTouch(views.rootContainer, event) + views.attachmentPager.dispatchTouchEvent(event) isOverlayWasClicked = dispatchOverlayTouch(event) } @@ -220,12 +223,12 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi private fun toggleOverlayViewVisibility() { if (systemUiVisibility) { // we hide - TransitionManager.beginDelayedTransition(rootContainer) + TransitionManager.beginDelayedTransition(views.rootContainer) hideSystemUI() overlayView?.isVisible = false } else { // we show - TransitionManager.beginDelayedTransition(rootContainer) + TransitionManager.beginDelayedTransition(views.rootContainer) showSystemUI() overlayView?.isVisible = true } @@ -238,11 +241,11 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi return when (swipeDirection) { SwipeDirection.Up, SwipeDirection.Down -> { if (isSwipeToDismissAllowed && !wasScaled && isImagePagerIdle) { - swipeDismissHandler.onTouch(rootContainer, event) + swipeDismissHandler.onTouch(views.rootContainer, event) } else true } SwipeDirection.Left, SwipeDirection.Right -> { - attachmentPager.dispatchTouchEvent(event) + views.attachmentPager.dispatchTouchEvent(event) } else -> true } @@ -250,8 +253,8 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi private fun handleSwipeViewMove(translationY: Float, translationLimit: Int) { val alpha = calculateTranslationAlpha(translationY, translationLimit) - backgroundView.alpha = alpha - dismissContainer.alpha = alpha + views.backgroundView.alpha = alpha + views.dismissContainer.alpha = alpha overlayView?.alpha = alpha } @@ -265,7 +268,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi private fun createSwipeToDismissHandler() : SwipeToDismissHandler = SwipeToDismissHandler( - swipeView = dismissContainer, + swipeView = views.dismissContainer, shouldAnimateDismiss = { shouldAnimateDismiss() }, onDismiss = { animateClose() }, onSwipeViewMove = ::handleSwipeViewMove) diff --git a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentsAdapter.kt b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentsAdapter.kt index 99c7777220..4805a1186b 100644 --- a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentsAdapter.kt +++ b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentsAdapter.kt @@ -98,7 +98,7 @@ class AttachmentsAdapter : RecyclerView.Adapter() { fun isScaled(position: Int): Boolean { val holder = recyclerView?.findViewHolderForAdapterPosition(position) if (holder is ZoomableImageViewHolder) { - return holder.touchImageView.attacher.scale > 1f + return holder.views.touchImageView.attacher.scale > 1f } return false } diff --git a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/ImageLoaderTarget.kt b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/ImageLoaderTarget.kt index 9166c2ce4f..531e8171e1 100644 --- a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/ImageLoaderTarget.kt +++ b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/ImageLoaderTarget.kt @@ -44,29 +44,29 @@ internal class DefaultImageLoaderTarget(val holder: AnimatedImageViewHolder, pri override fun onResourceLoading(uid: String, placeholder: Drawable?) { if (holder.boundResourceUid != uid) return - holder.imageLoaderProgress.isVisible = true + holder.views.imageLoaderProgress.isVisible = true } override fun onLoadFailed(uid: String, errorDrawable: Drawable?) { if (holder.boundResourceUid != uid) return - holder.imageLoaderProgress.isVisible = false - holder.touchImageView.setImageDrawable(errorDrawable) + holder.views.imageLoaderProgress.isVisible = false + holder.views.imageView.setImageDrawable(errorDrawable) } override fun onResourceCleared(uid: String, placeholder: Drawable?) { if (holder.boundResourceUid != uid) return - holder.touchImageView.setImageDrawable(placeholder) + holder.views.imageView.setImageDrawable(placeholder) } override fun onResourceReady(uid: String, resource: Drawable) { if (holder.boundResourceUid != uid) return - holder.imageLoaderProgress.isVisible = false + holder.views.imageLoaderProgress.isVisible = false // Glide mess up the view size :/ - holder.touchImageView.updateLayoutParams { + holder.views.imageView.updateLayoutParams { width = LinearLayout.LayoutParams.MATCH_PARENT height = LinearLayout.LayoutParams.MATCH_PARENT } - holder.touchImageView.setImageDrawable(resource) + holder.views.imageView.setImageDrawable(resource) if (resource is Animatable) { resource.start() } @@ -77,30 +77,30 @@ internal class DefaultImageLoaderTarget(val holder: AnimatedImageViewHolder, pri override fun onResourceLoading(uid: String, placeholder: Drawable?) { if (holder.boundResourceUid != uid) return - holder.imageLoaderProgress.isVisible = true - holder.touchImageView.setImageDrawable(placeholder) + holder.views.imageLoaderProgress.isVisible = true + holder.views.touchImageView.setImageDrawable(placeholder) } override fun onLoadFailed(uid: String, errorDrawable: Drawable?) { if (holder.boundResourceUid != uid) return - holder.imageLoaderProgress.isVisible = false - holder.touchImageView.setImageDrawable(errorDrawable) + holder.views.imageLoaderProgress.isVisible = false + holder.views.touchImageView.setImageDrawable(errorDrawable) } override fun onResourceCleared(uid: String, placeholder: Drawable?) { if (holder.boundResourceUid != uid) return - holder.touchImageView.setImageDrawable(placeholder) + holder.views.touchImageView.setImageDrawable(placeholder) } override fun onResourceReady(uid: String, resource: Drawable) { if (holder.boundResourceUid != uid) return - holder.imageLoaderProgress.isVisible = false + holder.views.imageLoaderProgress.isVisible = false // Glide mess up the view size :/ - holder.touchImageView.updateLayoutParams { + holder.views.touchImageView.updateLayoutParams { width = LinearLayout.LayoutParams.MATCH_PARENT height = LinearLayout.LayoutParams.MATCH_PARENT } - holder.touchImageView.setImageDrawable(resource) + holder.views.touchImageView.setImageDrawable(resource) } } } diff --git a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoLoaderTarget.kt b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoLoaderTarget.kt index d88faba110..078edfc548 100644 --- a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoLoaderTarget.kt +++ b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoLoaderTarget.kt @@ -49,19 +49,19 @@ internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val override fun onThumbnailResourceCleared(uid: String, placeholder: Drawable?) { if (holder.boundResourceUid != uid) return - holder.thumbnailImage.setImageDrawable(placeholder) + holder.views.videoThumbnailImage.setImageDrawable(placeholder) } override fun onThumbnailResourceReady(uid: String, resource: Drawable) { if (holder.boundResourceUid != uid) return - holder.thumbnailImage.setImageDrawable(resource) + holder.views.videoThumbnailImage.setImageDrawable(resource) } override fun onVideoFileLoading(uid: String) { if (holder.boundResourceUid != uid) return - holder.thumbnailImage.isVisible = true - holder.loaderProgressBar.isVisible = true - holder.videoView.isVisible = false + holder.views.videoThumbnailImage.isVisible = true + holder.views.videoLoaderProgress.isVisible = true + holder.views.videoView.isVisible = false } override fun onVideoFileLoadFailed(uid: String) { @@ -82,8 +82,8 @@ internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val } private fun arrangeForVideoReady() { - holder.thumbnailImage.isVisible = false - holder.loaderProgressBar.isVisible = false - holder.videoView.isVisible = true + holder.views.videoThumbnailImage.isVisible = false + holder.views.videoLoaderProgress.isVisible = false + holder.views.videoView.isVisible = true } } diff --git a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt index 4d8be6468b..0b72ef36f0 100644 --- a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt @@ -18,11 +18,8 @@ package im.vector.lib.attachmentviewer import android.util.Log 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 im.vector.lib.attachmentviewer.databinding.ItemVideoAttachmentBinding import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable @@ -44,13 +41,9 @@ class VideoViewHolder constructor(itemView: View) : var eventListener: WeakReference? = null - val thumbnailImage: ImageView = itemView.findViewById(R.id.videoThumbnailImage) - val videoView: VideoView = itemView.findViewById(R.id.videoView) - val loaderProgressBar: ProgressBar = itemView.findViewById(R.id.videoLoaderProgress) - val videoControlIcon: ImageView = itemView.findViewById(R.id.videoControlIcon) - val errorTextView: TextView = itemView.findViewById(R.id.videoMediaViewerErrorView) + val views = ItemVideoAttachmentBinding.bind(itemView) - internal val target = DefaultVideoLoaderTarget(this, thumbnailImage) + internal val target = DefaultVideoLoaderTarget(this, views.videoThumbnailImage) override fun onRecycled() { super.onRecycled() @@ -77,12 +70,12 @@ class VideoViewHolder constructor(itemView: View) : } override fun entersBackground() { - if (videoView.isPlaying) { - progress = videoView.currentPosition + if (views.videoView.isPlaying) { + progress = views.videoView.currentPosition progressDisposable?.dispose() progressDisposable = null - videoView.stopPlayback() - videoView.pause() + views.videoView.stopPlayback() + views.videoView.pause() } } @@ -92,9 +85,9 @@ class VideoViewHolder constructor(itemView: View) : override fun onSelected(selected: Boolean) { if (!selected) { - if (videoView.isPlaying) { - progress = videoView.currentPosition - videoView.stopPlayback() + if (views.videoView.isPlaying) { + progress = views.videoView.currentPosition + views.videoView.stopPlayback() } else { progress = 0 } @@ -109,34 +102,34 @@ class VideoViewHolder constructor(itemView: View) : } private fun startPlaying() { - thumbnailImage.isVisible = false - loaderProgressBar.isVisible = false - videoView.isVisible = true + views.videoThumbnailImage.isVisible = false + views.videoLoaderProgress.isVisible = false + views.videoView.isVisible = true - videoView.setOnPreparedListener { + views.videoView.setOnPreparedListener { progressDisposable?.dispose() progressDisposable = Observable.interval(100, TimeUnit.MILLISECONDS) .timeInterval() .observeOn(AndroidSchedulers.mainThread()) .subscribe { - val duration = videoView.duration - val progress = videoView.currentPosition - val isPlaying = videoView.isPlaying + val duration = views.videoView.duration + val progress = views.videoView.currentPosition + val isPlaying = views.videoView.isPlaying // Log.v("FOO", "isPlaying $isPlaying $progress/$duration") eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration)) } } try { - videoView.setVideoPath(mVideoPath) + views.videoView.setVideoPath(mVideoPath) } catch (failure: Throwable) { // Couldn't open Log.v(VideoViewHolder::class.java.name, "Failed to start video") } if (!wasPaused) { - videoView.start() + views.videoView.start() if (progress > 0) { - videoView.seekTo(progress) + views.videoView.seekTo(progress) } } } @@ -146,17 +139,17 @@ class VideoViewHolder constructor(itemView: View) : when (commands) { AttachmentCommands.StartVideo -> { wasPaused = false - videoView.start() + views.videoView.start() } AttachmentCommands.PauseVideo -> { wasPaused = true - videoView.pause() + views.videoView.pause() } is AttachmentCommands.SeekTo -> { - val duration = videoView.duration + val duration = views.videoView.duration if (duration > 0) { val seekDuration = duration * (commands.percentProgress / 100f) - videoView.seekTo(seekDuration.toInt()) + views.videoView.seekTo(seekDuration.toInt()) } } } diff --git a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/ZoomableImageViewHolder.kt b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/ZoomableImageViewHolder.kt index 49378631e8..2a212c16a6 100644 --- a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/ZoomableImageViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/ZoomableImageViewHolder.kt @@ -17,31 +17,29 @@ package im.vector.lib.attachmentviewer import android.view.View -import android.widget.ProgressBar -import com.github.chrisbanes.photoview.PhotoView +import im.vector.lib.attachmentviewer.databinding.ItemImageAttachmentBinding class ZoomableImageViewHolder constructor(itemView: View) : BaseViewHolder(itemView) { - val touchImageView: PhotoView = itemView.findViewById(R.id.touchImageView) - val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress) + val views = ItemImageAttachmentBinding.bind(itemView) init { - touchImageView.setAllowParentInterceptOnEdge(false) - touchImageView.setOnScaleChangeListener { scaleFactor, _, _ -> + views.touchImageView.setAllowParentInterceptOnEdge(false) + views.touchImageView.setOnScaleChangeListener { scaleFactor, _, _ -> // Log.v("ATTACHEMENTS", "scaleFactor $scaleFactor") // It's a bit annoying but when you pitch down the scaling // is not exactly one :/ - touchImageView.setAllowParentInterceptOnEdge(scaleFactor <= 1.0008f) + views.touchImageView.setAllowParentInterceptOnEdge(scaleFactor <= 1.0008f) } - touchImageView.setScale(1.0f, true) - touchImageView.setAllowParentInterceptOnEdge(true) + views.touchImageView.setScale(1.0f, true) + views.touchImageView.setAllowParentInterceptOnEdge(true) } - internal val target = DefaultImageLoaderTarget.ZoomableImageTarget(this, touchImageView) + internal val target = DefaultImageLoaderTarget.ZoomableImageTarget(this, views.touchImageView) override fun onRecycled() { super.onRecycled() - touchImageView.setImageDrawable(null) + views.touchImageView.setImageDrawable(null) } } diff --git a/build.gradle b/build.gradle index 6dd61a720c..7531dee61e 100644 --- a/build.gradle +++ b/build.gradle @@ -43,6 +43,10 @@ allprojects { includeGroupByRegex 'com\\.github\\.chrisbanes' // PFLockScreen-Android includeGroupByRegex 'com\\.github\\.vector-im' + + //Chat effects + includeGroupByRegex 'com\\.github\\.jetradarmobile' + includeGroupByRegex 'nl\\.dionsegijn' } } maven { diff --git a/docs/signin.md b/docs/signin.md index 06f715c46b..0a234d2a20 100644 --- a/docs/signin.md +++ b/docs/signin.md @@ -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 -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) @@ -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` -> 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 diff --git a/fastlane/metadata/android/en-US/changelogs/40100120.txt b/fastlane/metadata/android/en-US/changelogs/40100120.txt new file mode 100644 index 0000000000..39715c2910 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40100120.txt @@ -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 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/40100130.txt b/fastlane/metadata/android/en-US/changelogs/40100130.txt new file mode 100644 index 0000000000..39715c2910 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40100130.txt @@ -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 \ No newline at end of file diff --git a/fastlane/metadata/android/fi/changelogs/40100100.txt b/fastlane/metadata/android/fi/changelogs/40100100.txt new file mode 100644 index 0000000000..0717ff27d7 --- /dev/null +++ b/fastlane/metadata/android/fi/changelogs/40100100.txt @@ -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 diff --git a/fastlane/metadata/android/fi/full_description.txt b/fastlane/metadata/android/fi/full_description.txt new file mode 100644 index 0000000000..70def518da --- /dev/null +++ b/fastlane/metadata/android/fi/full_description.txt @@ -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 + +Miksi valita Element? + +OMAT TIEDOT: 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. + +AVOIN KOMMUNIKOINYI JA YHTEISTYÖ: 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. + +ERITTÄIN TURVALLINEN: Vahva päästä päähän -salaus (vain keskustelussa olevat voivat purkaa viestien salauksen), ja ristiin varmentaminen keskustelun osallistujien laitteiden tarkistamiseksi. + +TÄYDELLISTÄ VIESTINTÄÄ: 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. + +MISSÄ TAHANSA OLETKIN: Pidä yhteyttä missä tahansa, täysin synkronoidun viestihistorian kautta kaikilla laitteillasi ja verkossa osoitteessa https://app.element.io. diff --git a/fastlane/metadata/android/fi/short_description.txt b/fastlane/metadata/android/fi/short_description.txt index 64f35a7dff..5573ac1cb9 100644 --- a/fastlane/metadata/android/fi/short_description.txt +++ b/fastlane/metadata/android/fi/short_description.txt @@ -1 +1 @@ -Turvallista, hajautettua keskustelua ja VoIP-puheluita. Pidä tietosi turvassa. +Turvallista, hajautettua, keskusteluja ja VoIP-puheluita. Pidä tietosi turvassa. diff --git a/fastlane/metadata/android/it/changelogs/40100100.txt b/fastlane/metadata/android/it/changelogs/40100100.txt index 0c7cc8cc6c..5ca2f86e45 100644 --- a/fastlane/metadata/android/it/changelogs/40100100.txt +++ b/fastlane/metadata/android/it/changelogs/40100100.txt @@ -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 diff --git a/fastlane/metadata/android/pt_BR/changelogs/40100100.txt b/fastlane/metadata/android/pt_BR/changelogs/40100100.txt index 02cfd45a87..4884d7f62a 100644 --- a/fastlane/metadata/android/pt_BR/changelogs/40100100.txt +++ b/fastlane/metadata/android/pt_BR/changelogs/40100100.txt @@ -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 diff --git a/fastlane/metadata/android/sv/changelogs/40100100.txt b/fastlane/metadata/android/sv/changelogs/40100100.txt index 6da756aca9..afdef3583b 100644 --- a/fastlane/metadata/android/sv/changelogs/40100100.txt +++ b/fastlane/metadata/android/sv/changelogs/40100100.txt @@ -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 diff --git a/fastlane/metadata/android/uk/changelogs/40100100.txt b/fastlane/metadata/android/uk/changelogs/40100100.txt new file mode 100644 index 0000000000..e5333ae561 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40100100.txt @@ -0,0 +1,2 @@ +Ця версія містить переважно виправлення помилок та деякі покращення. Відправлення повідомлень стало тепер ще швидшим. +Повний перелік змін: https://github.com/vector-im/element-android/releases/tag/v1.0.10 diff --git a/fastlane/metadata/android/uk/full_description.txt b/fastlane/metadata/android/uk/full_description.txt index 64247581d2..026ae4162a 100644 --- a/fastlane/metadata/android/uk/full_description.txt +++ b/fastlane/metadata/android/uk/full_description.txt @@ -7,7 +7,7 @@ Element — це застосунок для спілкування та спі Element ґрунтовно відрізняється від інших застосунків для спілкування та співпраці тому що він є децентралізованим та відкритоджерельним. -Element дозволяє вам розміщувати сервер в себе або обирати будь-якого з надавачів послуг, таким чином забезпечуючи вам конфіденційність і можливість володіти власними даними й бесідами та контролювати їх. Він надає вам доступ до відкритої мережі, тож ви не є обмеженими спілкуванням виключно з користувачами Element. І він є дуже надійним та безпечним. +Element дозволяє вам розміщувати сервер в себе або обирати будь-якого з надавачів послуг, таким чином забезпечуючи вам конфіденційність і можливість володіти власними даними й бесідами та контролювати їх. Він надає вам доступ до відкритої мережі, тож ви не є обмеженими спілкуванням виключно з користувачами Element. І він є дуже надійним та безпечним. Element здатен забезпечити усе це завдяки тому, що він заснований на протоколі Matrix — стандарті для відкритого та децентралізованого спілкування. diff --git a/fastlane/metadata/android/zh_Hant/changelogs/40100100.txt b/fastlane/metadata/android/zh_Hant/changelogs/40100100.txt index 3c21bcbeb6..0ea092ba9a 100644 --- a/fastlane/metadata/android/zh_Hant/changelogs/40100100.txt +++ b/fastlane/metadata/android/zh_Hant/changelogs/40100100.txt @@ -1 +1,2 @@ -// 待辦事項 +這個新版本主要包含錯誤修復與改善。傳送訊息更快了。 +完整的變更紀錄請見:https://github.com/vector-im/element-android/releases/tag/v1.0.10 diff --git a/gradle.properties b/gradle.properties index b3f11e08a3..200866be25 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ org.gradle.jvmargs=-Xmx2048m org.gradle.vfs.watch=true 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 #vector.debugPrivateData=true diff --git a/matrix-sdk-android-rx/build.gradle b/matrix-sdk-android-rx/build.gradle index 37f41d0a2a..a99b5856ba 100644 --- a/matrix-sdk-android-rx/build.gradle +++ b/matrix-sdk-android-rx/build.gradle @@ -38,6 +38,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlin_coroutines_version" // Paging implementation "androidx.paging:paging-runtime-ktx:2.1.2" diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt index bf4bcacc31..b938f60e39 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt @@ -17,14 +17,20 @@ package org.matrix.android.sdk.rx 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.session.events.model.Event 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.members.RoomMemberQueryParams 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.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.RoomSummary 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.util.Optional 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) { @@ -121,28 +122,28 @@ class RxRoom(private val room: Room) { room.invite3pid(threePid, it) } - fun updateTopic(topic: String): Completable = completableBuilder { - room.updateTopic(topic, it) + fun updateTopic(topic: String): Completable = rxCompletable { + room.updateTopic(topic) } - fun updateName(name: String): Completable = completableBuilder { - room.updateName(name, it) + fun updateName(name: String): Completable = rxCompletable { + room.updateName(name) } - fun updateHistoryReadability(readability: RoomHistoryVisibility): Completable = completableBuilder { - room.updateHistoryReadability(readability, it) + fun updateHistoryReadability(readability: RoomHistoryVisibility): Completable = rxCompletable { + room.updateHistoryReadability(readability) } - fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?): Completable = completableBuilder { - room.updateJoinRule(joinRules, guestAccess, it) + fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?): Completable = rxCompletable { + room.updateJoinRule(joinRules, guestAccess) } - fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder { - room.updateAvatar(avatarUri, fileName, it) + fun updateAvatar(avatarUri: Uri, fileName: String): Completable = rxCompletable { + room.updateAvatar(avatarUri, fileName) } - fun deleteAvatar(): Completable = completableBuilder { - room.deleteAvatar(it) + fun deleteAvatar(): Completable = rxCompletable { + room.deleteAvatar() } } diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt index 0e5b88adb2..a7b269fcc6 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt @@ -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.rest.DeviceInfo 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) { @@ -139,7 +140,7 @@ class RxSession(private val session: Session) { } fun getRoomIdByAlias(roomAlias: String, - searchOnServer: Boolean): Single> = singleBuilder { + searchOnServer: Boolean): Single> = singleBuilder { session.getRoomIdByAlias(roomAlias, searchOnServer, it) } diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 18b0410167..e068c17d20 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' -apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-parcelize' apply plugin: 'realm-android' buildscript { @@ -9,14 +9,10 @@ buildscript { jcenter() } dependencies { - classpath "io.realm:realm-gradle-plugin:10.0.0" + classpath "io.realm:realm-gradle-plugin:10.1.2" } } -androidExtensions { - experimental = true -} - android { compileSdkVersion 29 testOptions.unitTests.includeAndroidResources = true @@ -63,7 +59,7 @@ android { release { 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" } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt index 0d71af864b..03943cea14 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt @@ -25,6 +25,7 @@ import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.BuildConfig 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.raw.RawService 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 olmManager: OlmManager @Inject internal lateinit var sessionManager: SessionManager + @Inject internal lateinit var homeServerHistoryService: HomeServerHistoryService private val uiHandler = Handler(Looper.getMainLooper()) @@ -71,6 +73,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo fun rawService() = rawService + fun homeServerHistoryService() = homeServerHistoryService + fun legacySessionImporter(): LegacySessionImporter { return legacySessionImporter } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt index e42059c639..da5e90abdd 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt @@ -17,13 +17,13 @@ package org.matrix.android.sdk.internal.crypto.encryption import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.runBlocking import org.amshove.kluent.shouldBe import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters 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.toContent import org.matrix.android.sdk.api.session.room.Room @@ -57,13 +57,14 @@ class EncryptionTest : InstrumentedTest { @Test fun test_EncryptionStateEvent() { performTest(roomShouldBeEncrypted = true) { room -> - // Send an encryption Event as a State Event - room.sendStateEvent( - eventType = EventType.STATE_ROOM_ENCRYPTION, - stateKey = null, - body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent(), - callback = NoOpMatrixCallback() - ) + runBlocking { + // Send an encryption Event as a State Event + room.sendStateEvent( + eventType = EventType.STATE_ROOM_ENCRYPTION, + stateKey = null, + body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent() + ) + } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt index 606f57b467..eb8b8b9730 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt @@ -264,7 +264,7 @@ class KeysBackupTest : InstrumentedTest { assertNotNull(decryption) // - Check decryptKeyBackupData() returns stg val sessionData = keysBackup - .decryptKeyBackupData(keyBackupData!!, + .decryptKeyBackupData(keyBackupData, session.olmInboundGroupSession!!.sessionIdentifier(), cryptoTestData.roomId, decryption!!) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt index 944d1036d3..b6e5ae7364 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt @@ -111,7 +111,7 @@ class KeysBackupTestHelper( Assert.assertTrue(keysBackup.isEnabled) stateObserver.stopAndCheckStates(null) - return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version!!) + return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version) } /** diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/media/UrlsExtractorTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/media/UrlsExtractorTest.kt new file mode 100644 index 0000000000..9ee84fdfc6 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/media/UrlsExtractorTest.kt @@ -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() + ) +} diff --git a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt index 630f6f1e29..34ed28d467 100644 --- a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt +++ b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.network.interceptors import androidx.annotation.NonNull -import org.matrix.android.sdk.BuildConfig import okhttp3.logging.HttpLoggingInterceptor import org.json.JSONArray import org.json.JSONException @@ -38,31 +37,28 @@ class FormattedJsonHttpLogger : HttpLoggingInterceptor.Logger { */ @Synchronized override fun log(@NonNull message: String) { - // In RELEASE there is no log, but for sure, test again BuildConfig.DEBUG - if (BuildConfig.DEBUG) { - Timber.v(message) + Timber.v(message) - if (message.startsWith("{")) { - // JSON Detected - try { - val o = JSONObject(message) - logJson(o.toString(INDENT_SPACE)) - } catch (e: JSONException) { - // Finally this is not a JSON string... - Timber.e(e) - } - } else if (message.startsWith("[")) { - // JSON Array detected - try { - val o = JSONArray(message) - logJson(o.toString(INDENT_SPACE)) - } catch (e: JSONException) { - // Finally not JSON... - Timber.e(e) - } + if (message.startsWith("{")) { + // JSON Detected + try { + val o = JSONObject(message) + logJson(o.toString(INDENT_SPACE)) + } catch (e: JSONException) { + // Finally this is not a JSON string... + Timber.e(e) + } + } else if (message.startsWith("[")) { + // JSON Array detected + try { + val o = JSONArray(message) + logJson(o.toString(INDENT_SPACE)) + } catch (e: JSONException) { + // Finally not JSON... + Timber.e(e) } - // Else not a json string to log } + // Else not a json string to log } private fun logJson(formattedJson: String) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt index cf6f37cec8..a5d457222f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt @@ -23,6 +23,7 @@ import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.BuildConfig 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.raw.RawService 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 olmManager: OlmManager @Inject internal lateinit var sessionManager: SessionManager + @Inject internal lateinit var homeServerHistoryService: HomeServerHistoryService init { Monarchy.init(context) @@ -65,6 +67,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo fun rawService() = rawService + fun homeServerHistoryService() = homeServerHistoryService + fun legacySessionImporter(): LegacySessionImporter { return legacySessionImporter } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt index 6cacf55a38..7d18aba627 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt @@ -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 * Ref: https://matrix.org/docs/spec/client_server/latest#login-fallback - * */ + */ 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 */ 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" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/HomeServerHistoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/HomeServerHistoryService.kt new file mode 100644 index 0000000000..77e33b8934 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/HomeServerHistoryService.kt @@ -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 + + fun addHomeServerToHistory(url: String) + + fun clearHistory() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt index 64d3ddcca5..f1f9ba3916 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.api.auth.data sealed class LoginFlowResult { data class Success( val supportedLoginTypes: List, + val ssoIdentityProviders: List?, val isLoginAndRegistrationSupported: Boolean, val homeServerUrl: String, val isOutdatedHomeserver: Boolean diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt new file mode 100644 index 0000000000..6759c59237 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt @@ -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" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawCacheStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/cache/CacheStrategy.kt similarity index 83% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawCacheStrategy.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/cache/CacheStrategy.kt index f4eada559e..2880d851d6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawCacheStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/cache/CacheStrategy.kt @@ -14,16 +14,16 @@ * 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 - object NoCache : RawCacheStrategy() + object NoCache : CacheStrategy() // 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 - 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 - object InfiniteCache : RawCacheStrategy() + object InfiniteCache : CacheStrategy() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt index 19549a338e..f1722b2189 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/raw/RawService.kt @@ -16,6 +16,8 @@ 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 */ @@ -23,7 +25,7 @@ interface RawService { /** * 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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index 56609610f1..8a95baf3cb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -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.identity.IdentityService 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.profile.ProfileService import org.matrix.android.sdk.api.session.pushers.PushersService @@ -181,6 +182,11 @@ interface Session : */ fun widgetService(): WidgetService + /** + * Returns the media service associated with the session + */ + fun mediaService(): MediaService + /** * Returns the integration manager service associated with the session */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt index 4677c2be32..98a84b8b66 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt @@ -20,7 +20,8 @@ import android.net.Uri import android.os.Parcelable import androidx.exifinterface.media.ExifInterface import com.squareup.moshi.JsonClass -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize +import org.matrix.android.sdk.api.util.MimeTypes.normalizeMimeType @Parcelize @JsonClass(generateAdapter = true) @@ -45,5 +46,5 @@ data class ContentAttachmentData( VIDEO } - fun getSafeMimeType() = if (mimeType == "image/jpg") "image/jpeg" else mimeType + fun getSafeMimeType() = mimeType?.normalizeMimeType() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt index 31f016be14..bcdb5ea257 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt @@ -18,8 +18,12 @@ package org.matrix.android.sdk.api.session.file import android.net.Uri 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.internal.crypto.attachments.ElementToDecrypt +import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt import java.io.File /** @@ -27,23 +31,6 @@ import java.io.File */ 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 { IN_CACHE, DOWNLOADING, @@ -54,34 +41,79 @@ interface FileService { * 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. */ - fun downloadFile( - downloadMode: DownloadMode, - id: String, - fileName: String, - mimeType: String?, - url: String?, - elementToDecrypt: ElementToDecrypt?, - callback: MatrixCallback): Cancelable + fun downloadFile(fileName: String, + mimeType: String?, + url: String?, + elementToDecrypt: ElementToDecrypt?, + callback: MatrixCallback): Cancelable - fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean + fun downloadFile(messageContent: MessageWithAttachmentContent, + callback: MatrixCallback): 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 * (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. * 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() + /** + * Clears all the decrypted files by the service + */ + fun clearDecryptedCache() + /** * Get size of cached files */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt new file mode 100644 index 0000000000..9040ec7d5c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/MediaService.kt @@ -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 + + /** + * 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() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/PreviewUrlData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/PreviewUrlData.kt new file mode 100644 index 0000000000..33fc8b052b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/PreviewUrlData.kt @@ -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`: + *
+ * {
+ *     "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"
+ * }
+ * 
+ */ +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? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt index ac1d726d03..aefc086b43 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt @@ -25,6 +25,7 @@ interface PermalinkService { companion object { const val MATRIX_TO_URL_BASE = "https://matrix.to/#/" + const val MATRIX_TO_CUSTOM_SCHEME_URL_BASE = "element://" } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt index 477bef66cf..5f02b77a1e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt @@ -18,12 +18,15 @@ package org.matrix.android.sdk.api.session.room import androidx.lifecycle.LiveData 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.model.RoomMemberSummary 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.peeking.PeekResult import org.matrix.android.sdk.api.util.Cancelable 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. @@ -120,7 +123,7 @@ interface RoomService { */ fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, - callback: MatrixCallback>): Cancelable + callback: MatrixCallback>): Cancelable /** * Delete a room alias @@ -163,4 +166,16 @@ interface RoomService { * @return a LiveData of the optional found room member */ fun getRoomMemberLive(userId: String, roomId: String): LiveData> + + /** + * Get some state events about a room + */ + fun getRoomState(roomId: String, callback: MatrixCallback>) + + /** + * 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) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt index 859f7fd104..73e27b64e3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt @@ -20,6 +20,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass 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.util.MimeTypes import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo @JsonClass(generateAdapter = true) @@ -54,5 +55,5 @@ data class MessageImageContent( @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null ) : MessageImageInfoContent { override val mimeType: String? - get() = encryptedFileInfo?.mimetype ?: info?.mimeType ?: "image/*" + get() = encryptedFileInfo?.mimetype ?: info?.mimeType ?: MimeTypes.Images } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt index 0f133323b0..a2b4e135d1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt @@ -33,4 +33,7 @@ object MessageType { // 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 const val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker" + + const val MSGTYPE_CONFETTI = "nic.custom.confetti" + const val MSGTYPE_SNOW = "nic.custom.snow" } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt new file mode 100644 index 0000000000..db70dadef3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt @@ -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 + ) : PeekResult() + + data class PeekingNotAllowed( + val roomId: String, + val alias: String?, + val viaServers: List + ) : PeekResult() + + object UnknownAlias : PeekResult() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt index 74e3faf38a..444366e912 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt @@ -18,13 +18,11 @@ package org.matrix.android.sdk.api.session.room.state import android.net.Uri 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.session.events.model.Event 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.RoomJoinRules -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.Optional @@ -33,41 +31,41 @@ interface StateService { /** * Update the topic of the room */ - fun updateTopic(topic: String, callback: MatrixCallback): Cancelable + suspend fun updateTopic(topic: String) /** * Update the name of the room */ - fun updateName(name: String, callback: MatrixCallback): Cancelable + suspend fun updateName(name: String) /** * Update the canonical alias of the 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. */ - fun updateCanonicalAlias(alias: String?, altAliases: List, callback: MatrixCallback): Cancelable + suspend fun updateCanonicalAlias(alias: String?, altAliases: List) /** * Update the history readability of the room */ - fun updateHistoryReadability(readability: RoomHistoryVisibility, callback: MatrixCallback): Cancelable + suspend fun updateHistoryReadability(readability: RoomHistoryVisibility) /** * Update the join rule and/or the guest access */ - fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?, callback: MatrixCallback): Cancelable + suspend fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?) /** * Update the avatar of the room */ - fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback): Cancelable + suspend fun updateAvatar(avatarUri: Uri, fileName: String) /** * Delete the avatar of the room */ - fun deleteAvatar(callback: MatrixCallback): Cancelable + suspend fun deleteAvatar() - fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback): Cancelable + suspend fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict) fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/EventTypeFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/EventTypeFilter.kt new file mode 100644 index 0000000000..18faa6a452 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/EventTypeFilter.kt @@ -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? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEventFilters.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEventFilters.kt index c751632286..4415c8e4b3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEventFilters.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEventFilters.kt @@ -36,5 +36,5 @@ data class TimelineEventFilters( /** * If [filterTypes] is true, the list of types allowed by the list. */ - val allowedTypes: List = emptyList() + val allowedTypes: List = emptyList() ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt new file mode 100644 index 0000000000..c74999b4ab --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt @@ -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() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt index c6d610188e..2ec8900f7c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt @@ -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.wellknown.WellknownModule import io.realm.RealmConfiguration +import org.matrix.android.sdk.api.auth.HomeServerHistoryService import java.io.File @Module(includes = [WellknownModule::class]) @@ -80,4 +81,7 @@ internal abstract class AuthModule { @Binds abstract fun bindDirectLoginTask(task: DefaultDirectLoginTask): DirectLoginTask + + @Binds + abstract fun bindHomeServerHistoryService(service: DefaultHomeServerHistoryService): HomeServerHistoryService } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index 3d5a0efcd4..55f053de8d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -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.HomeServerConnectionConfig 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.registration.RegistrationWizard import org.matrix.android.sdk.api.auth.wellknown.WellknownResult @@ -278,6 +279,7 @@ internal class DefaultAuthenticationService @Inject constructor( } return LoginFlowResult.Success( loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, + loginFlowResponse.flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl, !versions.isSupportedBySdk() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultHomeServerHistoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultHomeServerHistoryService.kt new file mode 100644 index 0000000000..7415938ebc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultHomeServerHistoryService.kt @@ -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 { + return monarchy.fetchAllMappedSync( + { realm -> + realm.where() + }, + { it.url } + ) + } + + override fun addHomeServerToHistory(url: String) { + monarchy.writeAsync { realm -> + KnownServerUrlEntity(url).let { + realm.insertOrUpdate(it) + } + } + } + + override fun clearHistory() { + monarchy.runTransactionSync { it.where().findAll().deleteAllFromRealm() } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt index 8acdee3608..2b26115f30 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.auth.data import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider @JsonClass(generateAdapter = true) internal data class LoginFlowResponse( @@ -34,5 +35,13 @@ internal data class LoginFlow( * The login type. This is supplied as the type when logging in. */ @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? ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/LocalizedFlowDataLoginTerms.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/LocalizedFlowDataLoginTerms.kt index 1e18887008..5d119bb617 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/LocalizedFlowDataLoginTerms.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/LocalizedFlowDataLoginTerms.kt @@ -17,7 +17,7 @@ package org.matrix.android.sdk.internal.auth.registration import android.os.Parcelable -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize /** * This class represent a localized privacy policy for registration Flow. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt index 5b105c4d40..3461a4d738 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt @@ -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. * 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. + * other example + * "params": { + * "m.login.sso": { + * "identity_providers": [ + * { + * "id": "google", + * "name": "Google", + * "icon": "https://..." + * } + * ] + * } + * } */ @Json(name = "params") val params: JsonDict? = null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/ElementToDecrypt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/ElementToDecrypt.kt index b77006aa3a..c071384df4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/ElementToDecrypt.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/ElementToDecrypt.kt @@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.crypto.attachments import android.os.Parcelable import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize fun EncryptedFileInfo.toElementToDecrypt(): ElementToDecrypt? { // Check the validity of some fields diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 973388da49..b970ec60e2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -20,6 +20,7 @@ import io.realm.DynamicRealm import io.realm.RealmMigration 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.PreviewUrlCacheEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import timber.log.Timber import javax.inject.Inject @@ -27,7 +28,7 @@ import javax.inject.Inject class RealmSessionStoreMigration @Inject constructor() : RealmMigration { 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) { @@ -38,6 +39,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { if (oldVersion <= 2) migrateTo3(realm) if (oldVersion <= 3) migrateTo4(realm) if (oldVersion <= 4) migrateTo5(realm) + if (oldVersion <= 5) migrateTo6(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -89,4 +91,18 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { ?.removeField("adminE2EByDefault") ?.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) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/KnownServerUrlEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/KnownServerUrlEntity.kt new file mode 100644 index 0000000000..1ebdc22ab4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/KnownServerUrlEntity.kt @@ -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 +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PreviewUrlCacheEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PreviewUrlCacheEntity.kt new file mode 100644 index 0000000000..b1e0b64405 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PreviewUrlCacheEntity.kt @@ -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 +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt index f62312f8fc..bca2c42c9e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -48,6 +48,7 @@ import io.realm.annotations.RealmModule PushRulesEntity::class, PushRuleEntity::class, PushConditionEntity::class, + PreviewUrlCacheEntity::class, PusherEntity::class, PusherDataEntity::class, ReadReceiptsSummaryEntity::class, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PreviewUrlCacheEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PreviewUrlCacheEntityQueries.kt new file mode 100644 index 0000000000..a139c17439 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PreviewUrlCacheEntityQueries.kt @@ -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() + .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) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt index 41a13c785d..148232cf94 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt @@ -71,8 +71,23 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm, } internal fun RealmQuery.filterEvents(filters: TimelineEventFilters): RealmQuery { - if (filters.filterTypes) { - `in`(TimelineEventEntityFields.ROOT.TYPE, filters.allowedTypes.toTypedArray()) + if (filters.filterTypes && filters.allowedTypes.isNotEmpty()) { + 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) { not() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt index d3f08fde36..9d6fa29bb2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt @@ -25,6 +25,7 @@ import okhttp3.OkHttpClient import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.MatrixConfiguration 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.internal.SessionManager import org.matrix.android.sdk.internal.auth.AuthModule @@ -62,6 +63,8 @@ internal interface MatrixComponent { fun rawService(): RawService + fun homeServerHistoryService(): HomeServerHistoryService + fun context(): Context fun matrixConfiguration(): MatrixConfiguration @@ -71,9 +74,6 @@ internal interface MatrixComponent { @CacheDirectory fun cacheDir(): File - @ExternalFilesDirectory - fun externalFilesDir(): File? - fun olmManager(): OlmManager fun taskExecutor(): TaskExecutor diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt index 71cbd8f1a1..b58fb3e683 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt @@ -57,13 +57,6 @@ internal object MatrixModule { return context.cacheDir } - @JvmStatic - @Provides - @ExternalFilesDirectory - fun providesExternalFilesDir(context: Context): File? { - return context.getExternalFilesDir(null) - } - @JvmStatic @Provides @MatrixScope diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt index e6cec7f7ac..2535a5347a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt @@ -16,14 +16,15 @@ 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.delay 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.awaitResponse +import timber.log.Timber import java.io.IOException internal suspend inline fun executeRequest(eventBus: EventBus?, @@ -49,6 +50,9 @@ internal class Request(private val eventBus: EventBus?) { throw response.toFailure(eventBus) } } 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 CertUtil.getCertificateException(exception) // TODO Support certificate error once logged diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultCleanRawCacheTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/CleanRawCacheTask.kt similarity index 100% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultCleanRawCacheTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/CleanRawCacheTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt index 3b0d7546e5..42b826de16 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultRawService.kt @@ -16,7 +16,7 @@ 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 java.util.concurrent.TimeUnit import javax.inject.Inject @@ -25,15 +25,15 @@ internal class DefaultRawService @Inject constructor( private val getUrlTask: GetUrlTask, private val cleanRawCacheTask: CleanRawCacheTask ) : RawService { - override suspend fun getUrl(url: String, rawCacheStrategy: RawCacheStrategy): String { - return getUrlTask.execute(GetUrlTask.Params(url, rawCacheStrategy)) + override suspend fun getUrl(url: String, cacheStrategy: CacheStrategy): String { + return getUrlTask.execute(GetUrlTask.Params(url, cacheStrategy)) } override suspend fun getWellknown(userId: String): String { val homeServerDomain = userId.substringAfter(":") return getUrl( "https://$homeServerDomain/.well-known/matrix/client", - RawCacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false) + CacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false) ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultGetUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GetUrlTask.kt similarity index 86% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultGetUrlTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GetUrlTask.kt index 1f4ca6d627..16633d90ef 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/DefaultGetUrlTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GetUrlTask.kt @@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.raw import com.zhuinden.monarchy.Monarchy 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.query.get import org.matrix.android.sdk.internal.database.query.getOrCreate @@ -32,7 +32,7 @@ import javax.inject.Inject internal interface GetUrlTask : Task { data class Params( val url: String, - val rawCacheStrategy: RawCacheStrategy + val cacheStrategy: CacheStrategy ) } @@ -42,14 +42,14 @@ internal class DefaultGetUrlTask @Inject constructor( ) : GetUrlTask { override suspend fun execute(params: GetUrlTask.Params): String { - return when (params.rawCacheStrategy) { - RawCacheStrategy.NoCache -> doRequest(params.url) - is RawCacheStrategy.TtlCache -> doRequestWithCache( + return when (params.cacheStrategy) { + CacheStrategy.NoCache -> doRequest(params.url) + is CacheStrategy.TtlCache -> doRequestWithCache( params.url, - params.rawCacheStrategy.validityDurationInMillis, - params.rawCacheStrategy.strict + params.cacheStrategy.validityDurationInMillis, + params.cacheStrategy.strict ) - RawCacheStrategy.InfiniteCache -> doRequestWithCache( + CacheStrategy.InfiniteCache -> doRequestWithCache( params.url, Long.MAX_VALUE, true diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmMigration.kt new file mode 100644 index 0000000000..49bcc72181 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmMigration.kt @@ -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) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmModule.kt index e4e4160193..770a49c904 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmModule.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.raw import io.realm.annotations.RealmModule +import org.matrix.android.sdk.internal.database.model.KnownServerUrlEntity 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, classes = [ - RawCacheEntity::class + RawCacheEntity::class, + KnownServerUrlEntity::class ]) internal class GlobalRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawModule.kt index aee2a52818..50721b809a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawModule.kt @@ -57,6 +57,9 @@ internal abstract class RawModule { realmKeysUtils.configureEncryption(this, DB_ALIAS) } .name("matrix-sdk-global.realm") + .schemaVersion(GlobalRealmMigration.SCHEMA_VERSION) + .migration(GlobalRealmMigration) + .allowWritesOnUiThread(true) .modules(GlobalRealmModule()) .build() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt index 861ae7c7ee..07cde3da60 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt @@ -21,6 +21,10 @@ import android.net.Uri import android.webkit.MimeTypeMap import androidx.core.content.FileProvider 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.extensions.tryOrNull 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.internal.crypto.attachments.ElementToDecrypt 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.UnauthenticatedWithCertificateWithProgress 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.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.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 java.io.File import java.io.IOException -import java.io.InputStream -import java.net.URLEncoder import javax.inject.Inject internal class DefaultFileService @Inject constructor( private val context: Context, - @CacheDirectory - private val cacheDirectory: File, - @ExternalFilesDirectory - private val externalFilesDirectory: File?, @SessionDownloadsDirectory private val sessionCacheDirectory: File, private val contentUrlResolver: ContentUrlResolver, @@ -67,9 +57,17 @@ internal class DefaultFileService @Inject constructor( private val taskExecutor: TaskExecutor ) : 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 @@ -81,28 +79,26 @@ internal class DefaultFileService @Inject constructor( * Download file in the cache folder, and eventually decrypt it * TODO looks like files are copied 3 times */ - override fun downloadFile(downloadMode: FileService.DownloadMode, - id: String, - fileName: String, + override fun downloadFile(fileName: String, mimeType: String?, url: String?, elementToDecrypt: ElementToDecrypt?, callback: MatrixCallback): Cancelable { - val unwrappedUrl = url ?: return NoOpCancellable.also { + url ?: return NoOpCancellable.also { callback.onFailure(IllegalArgumentException("url is null")) } - Timber.v("## FileService downloadFile $unwrappedUrl") + Timber.v("## FileService downloadFile $url") synchronized(ongoing) { - val existing = ongoing[unwrappedUrl] + val existing = ongoing[url] if (existing != null) { Timber.v("## FileService downloadFile is already downloading.. ") existing.add(callback) return NoOpCancellable } else { // mark as tracked - ongoing[unwrappedUrl] = ArrayList() + ongoing[url] = ArrayList() // and proceed to download } } @@ -110,15 +106,15 @@ internal class DefaultFileService @Inject constructor( return taskExecutor.executorScope.launch(coroutineDispatchers.main) { withContext(coroutineDispatchers.io) { Try { - if (!downloadFolder.exists()) { - downloadFolder.mkdirs() + if (!decryptedFolder.exists()) { + decryptedFolder.mkdirs() } // 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 // shared with will not function well (even if mime type is passed in the intent) - File(downloadFolder, fileForUrl(unwrappedUrl, mimeType)) - }.flatMap { destFile -> - if (!destFile.exists()) { + getFiles(url, fileName, mimeType, elementToDecrypt != null) + }.flatMap { cachedFiles -> + if (!cachedFiles.file.exists()) { val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null")) val request = Request.Builder() @@ -141,79 +137,153 @@ internal class DefaultFileService @Inject constructor( Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}") - if (elementToDecrypt != null) { - Timber.v("## FileService: decrypt file") - val decryptSuccess = destFile.outputStream().buffered().use { - MXEncryptedAttachments.decryptAttachment( - source.inputStream(), - elementToDecrypt, - it - ) - } - response.close() - if (!decryptSuccess) { - return@flatMap Try.Failure(IllegalStateException("Decryption error")) - } - } else { - writeToFile(source.inputStream(), destFile) - response.close() - } + // Write the file to cache (encrypted version if the file is encrypted) + writeToFile(source.inputStream(), cachedFiles.file) + response.close() } else { Timber.v("## FileService: cache hit for $url") } - Try.just(copyFile(destFile, downloadMode)) + Try.just(cachedFiles) } - }.fold({ - callback.onFailure(it) - // notify concurrent requests - val toNotify = synchronized(ongoing) { - ongoing[unwrappedUrl]?.also { - ongoing.remove(unwrappedUrl) + }.flatMap { cachedFiles -> + // Decrypt if necessary + if (cachedFiles.decryptedFile != null) { + if (!cachedFiles.decryptedFile.exists()) { + Timber.v("## FileService: decrypt file") + // 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 -> - tryOrNull { otherCallbacks.onFailure(it) } - } - }, { file -> - callback.onSuccess(file) - // notify concurrent requests - val toNotify = synchronized(ongoing) { - ongoing[unwrappedUrl]?.also { - ongoing.remove(unwrappedUrl) + }.fold( + { throwable -> + callback.onFailure(throwable) + // notify concurrent requests + val toNotify = synchronized(ongoing) { + ongoing[url]?.also { + ongoing.remove(url) + } + } + 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() } - fun storeDataFor(url: String, mimeType: String?, inputStream: InputStream) { - val file = File(downloadFolder, fileForUrl(url, mimeType)) - val source = inputStream.source().buffer() - file.sink().buffer().let { sink -> - source.use { input -> - sink.use { output -> - output.writeAll(input) + fun storeDataFor(mxcUrl: String, + filename: String?, + mimeType: String?, + originalFile: File, + encryptedFile: File?) { + val files = getFiles(mxcUrl, filename, mimeType, encryptedFile != null) + 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 { - val extension = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) } - return if (extension != null) "${url.safeFileName()}.$extension" else url.safeFileName() + override fun isFileInCache(mxcUrl: String?, + fileName: String, + mimeType: String?, + elementToDecrypt: ElementToDecrypt?): Boolean { + return fileState(mxcUrl, fileName, mimeType, elementToDecrypt) == FileService.FileState.IN_CACHE } - override fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean { - return File(downloadFolder, fileForUrl(mxcUrl, mimeType)).exists() + internal data class CachedFiles( + // 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 { - if (isFileInCache(mxcUrl, mimeType)) return FileService.FileState.IN_CACHE + private fun getFiles(mxcUrl: String, + 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) { 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 * (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? 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 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 { return downloadFolder.walkTopDown() .onEnter { @@ -256,4 +318,14 @@ internal class DefaultFileService @Inject constructor( override fun clearCache() { 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" + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index 25345e953c..c5f3f65a34 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -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.homeserver.HomeServerCapabilitiesService 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.profile.ProfileService import org.matrix.android.sdk.api.session.pushers.PushersService @@ -102,6 +103,7 @@ internal class DefaultSession @Inject constructor( private val permalinkService: Lazy, private val secureStorageService: Lazy, private val profileService: Lazy, + private val mediaService: Lazy, private val widgetService: Lazy, private val syncThreadProvider: Provider, private val contentUrlResolver: ContentUrlResolver, @@ -263,6 +265,8 @@ internal class DefaultSession @Inject constructor( override fun widgetService(): WidgetService = widgetService.get() + override fun mediaService(): MediaService = mediaService.get() + override fun integrationManagerService() = integrationManagerService override fun callSignalingService(): CallSignalingService = callSignalingService.get() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt index e6fd5a7a0c..659fcc8f5c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt @@ -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.identity.IdentityModule 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.profile.ProfileModule import org.matrix.android.sdk.internal.session.pushers.AddHttpPusherWorker @@ -75,6 +76,7 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers GroupModule::class, ContentModule::class, CacheModule::class, + MediaModule::class, CryptoModule::class, PushersModule::class, OpenIdModule::class, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index 32949d60c4..96b44917bd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -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.SessionRealmConfigurationFactory 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.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory @@ -169,9 +170,9 @@ internal abstract class SessionModule { @JvmStatic @Provides @SessionDownloadsDirectory - fun providesCacheDir(@SessionId sessionId: String, - context: Context): File { - return File(context.cacheDir, "downloads/$sessionId") + fun providesDownloadsCacheDir(@SessionId sessionId: String, + @CacheDirectory cacheFile: File): File { + return File(cacheFile, "downloads/$sessionId") } @JvmStatic diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentUploadResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentUploadResponse.kt index b5de26b39d..1ebe5b2eb6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentUploadResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ContentUploadResponse.kt @@ -20,6 +20,9 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @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 ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt index 8c3aad6a1f..4b31db59b1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt @@ -20,6 +20,7 @@ import android.content.Context import android.graphics.Bitmap import android.media.MediaMetadataRetriever import org.matrix.android.sdk.api.session.content.ContentAttachmentData +import org.matrix.android.sdk.api.util.MimeTypes import timber.log.Timber import java.io.ByteArrayOutputStream @@ -58,7 +59,7 @@ internal object ThumbnailExtractor { height = thumbnailHeight, size = thumbnailSize.toLong(), bytes = outputStream.toByteArray(), - mimeType = "image/jpeg" + mimeType = MimeTypes.Jpeg ) thumbnail.recycle() outputStream.reset() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 4a30d6c1e6..672d407d25 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -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.MessageImageContent 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.model.rest.EncryptedFileInfo import org.matrix.android.sdk.internal.database.mapper.ContentMapper @@ -151,7 +152,10 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter 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) .also { compressedFile -> // Get new Bitmap size @@ -174,14 +178,15 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } } + val encryptedFile: File? val contentUploadResponse = if (params.isEncrypted) { 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) } uploadedFileEncryptedFileInfo = - MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), attachment.getSafeMimeType(), tmpEncrypted) { read, total -> + MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), attachment.getSafeMimeType(), encryptedFile) { read, total -> notifyTracker(params) { contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong()) } @@ -190,18 +195,23 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter Timber.v("## FileService: Uploading file") fileUploader - .uploadFile(tmpEncrypted, attachment.name, "application/octet-stream", progressListener) + .uploadFile(encryptedFile, attachment.name, MimeTypes.OctetStream, progressListener) } else { Timber.v("## FileService: Clear file") + encryptedFile = null fileUploader .uploadFile(fileToUpload, attachment.name, attachment.getSafeMimeType(), progressListener) } Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}") try { - context.contentResolver.openInputStream(attachment.queryUri)?.let { - fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it) - } + fileService.storeDataFor( + mxcUrl = contentUploadResponse.contentUri, + filename = params.attachment.name, + mimeType = params.attachment.getSafeMimeType(), + originalFile = workingFile, + encryptedFile = encryptedFile + ) Timber.v("## FileService: cache storage updated") } catch (failure: Throwable) { 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 contentUploadResponse = fileUploader.uploadByteArray(encryptionResult.encryptedByteArray, "thumb_${params.attachment.name}", - "application/octet-stream", + MimeTypes.OctetStream, thumbnailProgressListener) UploadThumbnailResult( contentUploadResponse.contentUri, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultSaveFilterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt similarity index 100% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultSaveFilterTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGetGroupDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataTask.kt similarity index 100% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGetGroupDataTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt index 39b6608de3..8242edac84 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/CapabilitiesAPI.kt @@ -22,19 +22,12 @@ import retrofit2.Call import retrofit2.http.GET internal interface CapabilitiesAPI { - /** * Request the homeserver capabilities */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "capabilities") fun getCapabilities(): Call - /** - * Request the upload capabilities - */ - @GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config") - fun getUploadCapabilities(): Call - /** * Request the versions */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt similarity index 89% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index 8d289dfda5..f3686b02d3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -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.network.executeRequest 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.util.awaitTransaction import org.matrix.android.sdk.internal.wellknown.GetWellknownTask @@ -40,6 +42,7 @@ internal interface GetHomeServerCapabilitiesTask : Task internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( private val capabilitiesAPI: CapabilitiesAPI, + private val mediaAPI: MediaAPI, @SessionDatabase private val monarchy: Monarchy, private val eventBus: EventBus, private val getWellknownTask: GetWellknownTask, @@ -67,9 +70,9 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( } }.getOrNull() - val uploadCapabilities = runCatching { - executeRequest(eventBus) { - apiCall = capabilitiesAPI.getUploadCapabilities() + val mediaConfig = runCatching { + executeRequest(eventBus) { + apiCall = mediaAPI.getMediaConfig() } }.getOrNull() @@ -83,11 +86,11 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( getWellknownTask.execute(GetWellknownTask.Params(userId, homeServerConnectionConfig)) }.getOrNull() - insertInDb(capabilities, uploadCapabilities, versions, wellknownResult) + insertInDb(capabilities, mediaConfig, versions, wellknownResult) } private suspend fun insertInDb(getCapabilitiesResult: GetCapabilitiesResult?, - getUploadCapabilitiesResult: GetUploadCapabilitiesResult?, + getMediaConfigResult: GetMediaConfigResult?, getVersionResult: Versions?, getWellknownResult: WellknownResult?) { monarchy.awaitTransaction { realm -> @@ -97,8 +100,8 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword() } - if (getUploadCapabilitiesResult != null) { - homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize + if (getMediaConfigResult != null) { + homeServerCapabilitiesEntity.maxUploadFileSize = getMediaConfigResult.maxUploadSize ?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt index 3b0d514cf3..a03bef9501 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt @@ -93,7 +93,7 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor( } catch (failure: Throwable) { // Catch invalid hash pepper and retry 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()) { // Store it and use it right now hashDetailResponse.copy(pepper = failure.error.newLookupPepper) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/ClearPreviewUrlCacheTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/ClearPreviewUrlCacheTask.kt new file mode 100644 index 0000000000..004b622c64 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/ClearPreviewUrlCacheTask.kt @@ -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 + +internal class DefaultClearPreviewUrlCacheTask @Inject constructor( + @SessionDatabase private val monarchy: Monarchy +) : ClearPreviewUrlCacheTask { + + override suspend fun execute(params: Unit) { + monarchy.awaitTransaction { realm -> + realm.where() + .findAll() + .deleteAllFromRealm() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt new file mode 100644 index 0000000000..1a400ccfcf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/DefaultMediaService.kt @@ -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>(1_000) + + override fun extractUrls(event: Event): List { + 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) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetUploadCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetMediaConfigResult.kt similarity index 86% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetUploadCapabilitiesResult.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetMediaConfigResult.kt index 92903bf96e..fece6c06c6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetUploadCapabilitiesResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetMediaConfigResult.kt @@ -5,7 +5,7 @@ * 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 + * 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, @@ -14,13 +14,13 @@ * 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.JsonClass @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. * If not listed or null, the size limit should be treated as unknown. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt new file mode 100644 index 0000000000..69cdfa8faa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt @@ -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 { + 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(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 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetRawPreviewUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetRawPreviewUrlTask.kt new file mode 100644 index 0000000000..6c5dad2422 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetRawPreviewUrlTask.kt @@ -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 { + 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) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt new file mode 100644 index 0000000000..bbb4f1e06a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaAPI.kt @@ -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 + + /** + * 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 +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaModule.kt new file mode 100644 index 0000000000..bc58b3f444 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/MediaModule.kt @@ -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 +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt new file mode 100644 index 0000000000..dd1a9ead26 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt @@ -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 +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt new file mode 100644 index 0000000000..e531d6af9f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/UrlsExtractor.kt @@ -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 { + return event.takeIf { it.getClearType() == EventType.MESSAGE } + ?.getClearContent() + ?.toModel() + ?.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() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt index 5265e4f17d..500d43408e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt @@ -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.util.Cancelable 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.internal.database.model.PendingThreePidEntity 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): Cancelable { 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)) userStore.updateAvatar(userId, response.contentUri) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt index 9ec985e0b6..383dd876d3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt @@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import com.zhuinden.monarchy.Monarchy 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.RoomService 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.RoomSummary 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.Optional 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.session.room.alias.DeleteRoomAliasTask 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.membership.RoomChangeMembershipStateDataSource 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.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.summary.RoomSummaryDataSource 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 roomIdByAliasTask: GetRoomIdByAliasTask, private val deleteRoomAliasTask: DeleteRoomAliasTask, + private val resolveRoomStateTask: ResolveRoomStateTask, + private val peekRoomTask: PeekRoomTask, private val roomGetter: RoomGetter, private val roomSummaryDataSource: RoomSummaryDataSource, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, @@ -119,7 +126,7 @@ internal class DefaultRoomService @Inject constructor( .executeBy(taskExecutor) } - override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback>): Cancelable { + override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback>): Cancelable { return roomIdByAliasTask .configureWith(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer)) { this.callback = callback @@ -154,4 +161,20 @@ internal class DefaultRoomService @Inject constructor( results.firstOrNull().toOptional() } } + + override fun getRoomState(roomId: String, callback: MatrixCallback>) { + resolveRoomStateTask + .configureWith(ResolveRoomStateTask.Params(roomId)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + + override fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback) { + peekRoomTask + .configureWith(PeekRoomTask.Params(roomIdOrAlias)) { + this.callback = callback + } + .executeBy(taskExecutor) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index 955a251b52..aa92c1cb3b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -183,7 +183,7 @@ internal interface RoomAPI { @Body body: ThreePidInviteBody): Call /** - * Send a generic state events + * Send a generic state event * * @param roomId the room id. * @param stateEventType the state event type @@ -195,7 +195,7 @@ internal interface RoomAPI { @Body params: JsonDict): Call /** - * Send a generic state events + * Send a generic state event * * @param roomId the room id. * @param stateEventType the state event type @@ -208,6 +208,13 @@ internal interface RoomAPI { @Path("state_key") stateKey: String, @Body params: JsonDict): Call + /** + * Get state events of a room + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-state + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/state") + fun getRoomState(@Path("roomId") roomId: String) : Call> + /** * Send a relation event to a room. * diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index 3a94396a61..92f4ea2aea 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -57,6 +57,10 @@ import org.matrix.android.sdk.internal.session.room.membership.leaving.DefaultLe import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask import org.matrix.android.sdk.internal.session.room.membership.threepid.DefaultInviteThreePidTask import org.matrix.android.sdk.internal.session.room.membership.threepid.InviteThreePidTask +import org.matrix.android.sdk.internal.session.room.peeking.DefaultPeekRoomTask +import org.matrix.android.sdk.internal.session.room.peeking.DefaultResolveRoomStateTask +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.DefaultMarkAllRoomsReadTask import org.matrix.android.sdk.internal.session.room.read.DefaultSetReadMarkersTask import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask @@ -223,4 +227,10 @@ internal abstract class RoomModule { @Binds abstract fun bindDeleteTagFromRoomTask(task: DefaultDeleteTagFromRoomTask): DeleteTagFromRoomTask + + @Binds + abstract fun bindResolveRoomStateTask(task: DefaultResolveRoomStateTask): ResolveRoomStateTask + + @Binds + abstract fun bindPeekRoomTask(task: DefaultPeekRoomTask): PeekRoomTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt index 3c47ee6ef0..543d605707 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt @@ -29,7 +29,7 @@ import org.matrix.android.sdk.internal.session.directory.DirectoryAPI import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject -internal interface GetRoomIdByAliasTask : Task> { +internal interface GetRoomIdByAliasTask : Task> { data class Params( val roomAlias: String, val searchOnServer: Boolean @@ -42,21 +42,21 @@ internal class DefaultGetRoomIdByAliasTask @Inject constructor( private val eventBus: EventBus ) : GetRoomIdByAliasTask { - override suspend fun execute(params: GetRoomIdByAliasTask.Params): Optional { - var roomId = Realm.getInstance(monarchy.realmConfiguration).use { + override suspend fun execute(params: GetRoomIdByAliasTask.Params): Optional { + val roomId = Realm.getInstance(monarchy.realmConfiguration).use { RoomSummaryEntity.findByAlias(it, params.roomAlias)?.roomId } return if (roomId != null) { - Optional.from(roomId) + Optional.from(RoomAliasDescription(roomId)) } else if (!params.searchOnServer) { - Optional.from(null) + Optional.from(null) } else { - roomId = tryOrNull("## Failed to get roomId from alias") { + val description = tryOrNull("## Failed to get roomId from alias") { executeRequest(eventBus) { apiCall = directoryAPI.getRoomIdByAlias(params.roomAlias) } - }?.roomId - Optional.from(roomId) + } + Optional.from(description) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt index ada3839fa0..d1f93c50be 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt @@ -20,7 +20,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -internal data class RoomAliasDescription( +data class RoomAliasDescription( /** * The room ID for this alias. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt index 79ff9db087..fb840b4eb3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.api.session.identity.toMedium import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.di.AuthenticatedIdentity @@ -96,7 +97,7 @@ internal class CreateRoomBodyBuilder @Inject constructor( fileUploader.uploadFromUri( uri = avatarUri, filename = UUID.randomUUID().toString(), - mimeType = "image/jpeg") + mimeType = MimeTypes.Jpeg) } ?.let { response -> Event( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt new file mode 100644 index 0000000000..5a82d74537 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt @@ -0,0 +1,145 @@ +/* + * 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.room.peeking + +import org.matrix.android.sdk.api.MatrixPatterns +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.RoomAvatarContent +import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent +import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomNameContent +import org.matrix.android.sdk.api.session.room.model.RoomTopicContent +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsFilter +import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams +import org.matrix.android.sdk.api.session.room.peeking.PeekResult +import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask +import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask +import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface PeekRoomTask : Task { + data class Params( + val roomIdOrAlias: String + ) +} + +internal class DefaultPeekRoomTask @Inject constructor( + private val getRoomIdByAliasTask: GetRoomIdByAliasTask, + private val getRoomDirectoryVisibilityTask: GetRoomDirectoryVisibilityTask, + private val getPublicRoomTask: GetPublicRoomTask, + private val resolveRoomStateTask: ResolveRoomStateTask +) : PeekRoomTask { + + override suspend fun execute(params: PeekRoomTask.Params): PeekResult { + val roomId: String + val serverList: List + val isAlias = MatrixPatterns.isRoomAlias(params.roomIdOrAlias) + if (isAlias) { + // get alias description + val aliasDescription = getRoomIdByAliasTask + .execute(GetRoomIdByAliasTask.Params(params.roomIdOrAlias, true)) + .getOrNull() + ?: return PeekResult.UnknownAlias + + roomId = aliasDescription.roomId + serverList = aliasDescription.servers + } else { + roomId = params.roomIdOrAlias + serverList = emptyList() + } + + // Is it a public room? + val publicRepoResult = when (getRoomDirectoryVisibilityTask.execute(GetRoomDirectoryVisibilityTask.Params(roomId))) { + RoomDirectoryVisibility.PRIVATE -> { + // We cannot resolve this room :/ + null + } + RoomDirectoryVisibility.PUBLIC -> { + // Try to find it in directory + val filter = if (isAlias) PublicRoomsFilter(searchTerm = params.roomIdOrAlias.substring(1)) + else null + + getPublicRoomTask.execute(GetPublicRoomTask.Params( + server = serverList.firstOrNull(), + publicRoomsParams = PublicRoomsParams( + filter = filter, + limit = 20.takeIf { filter != null } ?: 100 + ) + )).chunk?.firstOrNull { it.roomId == roomId } + } + } + + if (publicRepoResult != null) { + return PeekResult.Success( + roomId = roomId, + alias = publicRepoResult.getPrimaryAlias() ?: params.roomIdOrAlias.takeIf { isAlias }, + avatarUrl = publicRepoResult.avatarUrl, + name = publicRepoResult.name, + topic = publicRepoResult.topic, + numJoinedMembers = publicRepoResult.numJoinedMembers, + viaServers = serverList + ) + } + + // mm... try to peek state ? maybe the room is not public but yet allow guest to get events? + // this could be slow + try { + val stateEvents = resolveRoomStateTask.execute(ResolveRoomStateTask.Params(roomId)) + val name = stateEvents + .lastOrNull { it.type == EventType.STATE_ROOM_NAME && it.stateKey == "" } + ?.let { it.content?.toModel()?.name } + + val topic = stateEvents + .lastOrNull { it.type == EventType.STATE_ROOM_TOPIC && it.stateKey == "" } + ?.let { it.content?.toModel()?.topic } + + val avatarUrl = stateEvents + .lastOrNull { it.type == EventType.STATE_ROOM_AVATAR } + ?.let { it.content?.toModel()?.avatarUrl } + + val alias = stateEvents + .lastOrNull { it.type == EventType.STATE_ROOM_CANONICAL_ALIAS } + ?.let { it.content?.toModel()?.canonicalAlias } + + // not sure if it's the right way to do that :/ + val memberCount = stateEvents + .filter { it.type == EventType.STATE_ROOM_MEMBER && it.stateKey?.isNotEmpty() == true } + .distinctBy { it.stateKey } + .count() + + return PeekResult.Success( + roomId = roomId, + alias = alias, + avatarUrl = avatarUrl, + name = name, + topic = topic, + numJoinedMembers = memberCount, + viaServers = serverList + ) + } catch (failure: Throwable) { + // Would be M_FORBIDDEN if cannot peek :/ + // User XXX not in room !XXX, and room previews are disabled + return PeekResult.PeekingNotAllowed( + roomId = roomId, + alias = params.roomIdOrAlias.takeIf { isAlias }, + viaServers = serverList + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt new file mode 100644 index 0000000000..03ea2408f0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/ResolveRoomStateTask.kt @@ -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.room.peeking + +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface ResolveRoomStateTask : Task> { + data class Params( + val roomId: String + ) +} + +internal class DefaultResolveRoomStateTask @Inject constructor( + private val roomAPI: RoomAPI, + private val eventBus: EventBus +) : ResolveRoomStateTask { + + override suspend fun execute(params: ResolveRoomStateTask.Params): List { + return executeRequest(eventBus) { + apiCall = roomAPI.getRoomState(params.roomId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index b13ce15da6..8828f3dfed 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -177,7 +177,7 @@ internal class DefaultSendService @AssistedInject constructor( val attachmentData = ContentAttachmentData( size = messageContent.info!!.size, mimeType = messageContent.info.mimeType!!, - name = messageContent.body, + name = messageContent.getFileName(), queryUri = Uri.parse(messageContent.url), type = ContentAttachmentData.Type.FILE ) @@ -210,6 +210,8 @@ internal class DefaultSendService @AssistedInject constructor( override fun cancelSend(eventId: String) { cancelSendTracker.markLocalEchoForCancel(eventId, roomId) + // This is maybe the current task, so cancel it too + eventSenderProcessor.cancel(eventId, roomId) taskExecutor.executorScope.launch { localEchoRepository.deleteFailedEcho(roomId, eventId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt index 62e225c624..5014d94558 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.room.send.queue +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.auth.data.SessionParams @@ -106,17 +107,21 @@ internal class EventSenderProcessor @Inject constructor( // non blocking add to queue sendingQueue.add(task) markAsManaged(task) - return object : Cancelable { - override fun cancel() { - task.cancel() - } - } + return task + } + + fun cancel(eventId: String, roomId: String) { + (currentTask as? SendEventQueuedTask) + ?.takeIf { it -> it.event.eventId == eventId && it.event.roomId == roomId } + ?.cancel() } companion object { private const val RETRY_WAIT_TIME_MS = 10_000L } + private var currentTask: QueuedTask? = null + private var sendingQueue = LinkedBlockingQueue() private var networkAvailableLock = Object() @@ -129,6 +134,7 @@ internal class EventSenderProcessor @Inject constructor( while (!isInterrupted) { Timber.v("## SendThread wait for task to process") val task = sendingQueue.take() + .also { currentTask = it } Timber.v("## SendThread Found task to process $task") if (task.isCancelled()) { @@ -183,6 +189,10 @@ internal class EventSenderProcessor @Inject constructor( task.onTaskFailed() throw InterruptedException() } + exception is CancellationException -> { + Timber.v("## SendThread task has been cancelled") + break@retryLoop + } else -> { Timber.v("## SendThread retryLoop Un-Retryable error, try next task") // this task is in error, check next one? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt index e69c65ec4c..dfbac347d9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.session.room.send.queue import android.content.Context -import org.matrix.android.sdk.api.auth.data.sessionId import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.room.send.SendState diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt index bccbc97ff4..9a7fcd8d91 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt @@ -16,14 +16,26 @@ package org.matrix.android.sdk.internal.session.room.send.queue -abstract class QueuedTask { +import org.matrix.android.sdk.api.util.Cancelable + +abstract class QueuedTask : Cancelable { var retryCount = 0 - abstract suspend fun execute() + private var hasBeenCancelled: Boolean = false + + suspend fun execute() { + if (!isCancelled()) { + doExecute() + } + } + + abstract suspend fun doExecute() abstract fun onTaskFailed() - abstract fun isCancelled() : Boolean + open fun isCancelled() = hasBeenCancelled - abstract fun cancel() + final override fun cancel() { + hasBeenCancelled = true + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt index a3c19a1f7c..8e7ba2f155 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt @@ -22,20 +22,18 @@ import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository internal class RedactQueuedTask( - val toRedactEventId: String, + private val toRedactEventId: String, val redactionLocalEchoId: String, - val roomId: String, - val reason: String?, - val redactEventTask: RedactEventTask, - val localEchoRepository: LocalEchoRepository, - val cancelSendTracker: CancelSendTracker + private val roomId: String, + private val reason: String?, + private val redactEventTask: RedactEventTask, + private val localEchoRepository: LocalEchoRepository, + private val cancelSendTracker: CancelSendTracker ) : QueuedTask() { - private var _isCancelled: Boolean = false + override fun toString() = "[RedactQueuedTask $redactionLocalEchoId]" - override fun toString() = "[RedactEventRunnableTask $redactionLocalEchoId]" - - override suspend fun execute() { + override suspend fun doExecute() { redactEventTask.execute(RedactEventTask.Params(redactionLocalEchoId, roomId, toRedactEventId, reason)) } @@ -44,10 +42,6 @@ internal class RedactQueuedTask( } override fun isCancelled(): Boolean { - return _isCancelled || cancelSendTracker.isCancelRequestedFor(redactionLocalEchoId, roomId) - } - - override fun cancel() { - _isCancelled = true + return super.isCancelled() || cancelSendTracker.isCancelRequestedFor(redactionLocalEchoId, roomId) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt index 21a4145a9d..ea097082c7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt @@ -33,11 +33,9 @@ internal class SendEventQueuedTask( val cancelSendTracker: CancelSendTracker ) : QueuedTask() { - private var _isCancelled: Boolean = false + override fun toString() = "[SendEventQueuedTask ${event.eventId}]" - override fun toString() = "[SendEventRunnableTask ${event.eventId}]" - - override suspend fun execute() { + override suspend fun doExecute() { sendEventTask.execute(SendEventTask.Params(event, encrypt)) } @@ -56,10 +54,6 @@ internal class SendEventQueuedTask( } override fun isCancelled(): Boolean { - return _isCancelled || cancelSendTracker.isCancelRequestedFor(event.eventId, event.roomId) - } - - override fun cancel() { - _isCancelled = true + return super.isCancelled() || cancelSendTracker.isCancelRequestedFor(event.eventId, event.roomId) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt index 6015d945c4..b546584450 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt @@ -20,7 +20,6 @@ import android.net.Uri import androidx.lifecycle.LiveData import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject -import org.matrix.android.sdk.api.MatrixCallback 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.EventType @@ -32,22 +31,15 @@ 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.RoomJoinRulesContent import org.matrix.android.sdk.api.session.room.state.StateService -import org.matrix.android.sdk.api.util.Cancelable 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.internal.session.content.FileUploader import org.matrix.android.sdk.internal.session.room.alias.AddRoomAliasTask -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith -import org.matrix.android.sdk.internal.task.launchToCallback -import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers -import org.matrix.android.sdk.internal.util.awaitCallback internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String, private val stateEventDataSource: StateEventDataSource, - private val taskExecutor: TaskExecutor, private val sendStateTask: SendStateTask, - private val coroutineDispatchers: MatrixCoroutineDispatchers, private val fileUploader: FileUploader, private val addRoomAliasTask: AddRoomAliasTask ) : StateService { @@ -73,45 +65,38 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private return stateEventDataSource.getStateEventsLive(roomId, eventTypes, stateKey) } - override fun sendStateEvent( + override suspend fun sendStateEvent( eventType: String, stateKey: String?, - body: JsonDict, - callback: MatrixCallback - ): Cancelable { + body: JsonDict + ) { val params = SendStateTask.Params( roomId = roomId, stateKey = stateKey, eventType = eventType, body = body ) - return sendStateTask - .configureWith(params) { - this.callback = callback - } - .executeBy(taskExecutor) + sendStateTask.execute(params) } - override fun updateTopic(topic: String, callback: MatrixCallback): Cancelable { - return sendStateEvent( + override suspend fun updateTopic(topic: String) { + sendStateEvent( eventType = EventType.STATE_ROOM_TOPIC, body = mapOf("topic" to topic), - callback = callback, stateKey = null ) } - override fun updateName(name: String, callback: MatrixCallback): Cancelable { - return sendStateEvent( + override suspend fun updateName(name: String) { + sendStateEvent( eventType = EventType.STATE_ROOM_NAME, body = mapOf("name" to name), - callback = callback, stateKey = null ) } - override fun updateCanonicalAlias(alias: String?, altAliases: List, callback: MatrixCallback): Cancelable { - return sendStateEvent( + override suspend fun updateCanonicalAlias(alias: String?, altAliases: List) { + sendStateEvent( eventType = EventType.STATE_ROOM_CANONICAL_ALIAS, body = RoomCanonicalAliasContent( canonicalAlias = alias, @@ -123,64 +108,48 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private // Sort for the cleanup .sorted() ).toContent(), - callback = callback, stateKey = null ) } - override fun updateHistoryReadability(readability: RoomHistoryVisibility, callback: MatrixCallback): Cancelable { - return sendStateEvent( + override suspend fun updateHistoryReadability(readability: RoomHistoryVisibility) { + sendStateEvent( eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY, body = mapOf("history_visibility" to readability), - callback = callback, stateKey = null ) } - override fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?, callback: MatrixCallback): Cancelable { - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { - if (joinRules != null) { - awaitCallback { - sendStateEvent( - eventType = EventType.STATE_ROOM_JOIN_RULES, - body = RoomJoinRulesContent(joinRules).toContent(), - callback = it, - stateKey = null - ) - } - } - if (guestAccess != null) { - awaitCallback { - sendStateEvent( - eventType = EventType.STATE_ROOM_GUEST_ACCESS, - body = RoomGuestAccessContent(guestAccess).toContent(), - callback = it, - stateKey = null - ) - } - } + override suspend fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?) { + if (joinRules != null) { + sendStateEvent( + eventType = EventType.STATE_ROOM_JOIN_RULES, + body = RoomJoinRulesContent(joinRules).toContent(), + stateKey = null + ) + } + if (guestAccess != null) { + sendStateEvent( + eventType = EventType.STATE_ROOM_GUEST_ACCESS, + body = RoomGuestAccessContent(guestAccess).toContent(), + stateKey = null + ) } } - override fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback): Cancelable { - return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { - val response = fileUploader.uploadFromUri(avatarUri, fileName, "image/jpeg") - awaitCallback { - sendStateEvent( - eventType = EventType.STATE_ROOM_AVATAR, - body = mapOf("url" to response.contentUri), - callback = it, - stateKey = null - ) - } - } + override suspend fun updateAvatar(avatarUri: Uri, fileName: String) { + val response = fileUploader.uploadFromUri(avatarUri, fileName, MimeTypes.Jpeg) + sendStateEvent( + eventType = EventType.STATE_ROOM_AVATAR, + body = mapOf("url" to response.contentUri), + stateKey = null + ) } - override fun deleteAvatar(callback: MatrixCallback): Cancelable { - return sendStateEvent( + override suspend fun deleteAvatar() { + sendStateEvent( eventType = EventType.STATE_ROOM_AVATAR, body = emptyMap(), - callback = callback, stateKey = null ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryEventsHelper.kt index a3862b001b..7437a686da 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryEventsHelper.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.summary import io.realm.Realm import org.matrix.android.sdk.api.session.room.summary.RoomSummaryConstants +import org.matrix.android.sdk.api.session.room.timeline.EventTypeFilter import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.query.latestEvent @@ -26,7 +27,7 @@ internal object RoomSummaryEventsHelper { private val previewFilters = TimelineEventFilters( filterTypes = true, - allowedTypes = RoomSummaryConstants.PREVIEWABLE_TYPES, + allowedTypes = RoomSummaryConstants.PREVIEWABLE_TYPES.map { EventTypeFilter(eventType = it, stateKey = null) }, filterUseless = true, filterRedacted = false, filterEdits = true diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 995a21aa23..86b0497bd0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -784,19 +784,20 @@ internal class DefaultTimeline( } private fun List.filterEventsWithSettings(): List { - return filter { - val filterType = !settings.filters.filterTypes || settings.filters.allowedTypes.contains(it.root.type) + return filter { event -> + val filterType = !settings.filters.filterTypes + || settings.filters.allowedTypes.any { it.eventType == event.root.type && (it.stateKey == null || it.stateKey == event.root.senderId) } if (!filterType) return@filter false - val filterEdits = if (settings.filters.filterEdits && it.root.getClearType() == EventType.MESSAGE) { - val messageContent = it.root.getClearContent().toModel() + val filterEdits = if (settings.filters.filterEdits && event.root.getClearType() == EventType.MESSAGE) { + val messageContent = event.root.getClearContent().toModel() messageContent?.relatesTo?.type != RelationType.REPLACE && messageContent?.relatesTo?.type != RelationType.RESPONSE } else { true } if (!filterEdits) return@filter false - val filterRedacted = settings.filters.filterRedacted && it.root.isRedacted() + val filterRedacted = settings.filters.filterRedacted && event.root.isRedacted() !filterRedacted } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultGetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt similarity index 100% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultGetContextOfEventTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultPaginationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt similarity index 100% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultPaginationTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineHiddenReadReceipts.kt index 3dcc5e21b1..fa517bebf2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineHiddenReadReceipts.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -151,8 +151,25 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu beginGroup() var needOr = false if (settings.filters.filterTypes) { - val allowedTypes = settings.filters.allowedTypes.toTypedArray() - not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", allowedTypes) + beginGroup() + // Events: A, B, C, D, (E and S1), F, G, (H and S1), I + // Allowed: A, B, C, (E and S1), G, (H and S2) + // Result: D, F, H, I + settings.filters.allowedTypes.forEachIndexed { index, filter -> + if (filter.stateKey == null) { + notEqualTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", filter.eventType) + } else { + beginGroup() + notEqualTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", filter.eventType) + or() + notEqualTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.STATE_KEY}", filter.stateKey) + endGroup() + } + if (index != settings.filters.allowedTypes.size - 1) { + and() + } + } + endGroup() needOr = true } if (settings.filters.filterUseless) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt index 74cba5e796..424c24663c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt @@ -25,7 +25,6 @@ import org.matrix.android.sdk.api.failure.isTokenError import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker import org.matrix.android.sdk.internal.session.sync.SyncTask -import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver import org.matrix.android.sdk.internal.util.Debouncer import org.matrix.android.sdk.internal.util.createUIHandler @@ -50,14 +49,13 @@ private const val RETRY_WAIT_TIME_MS = 10_000L private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L internal class SyncThread @Inject constructor(private val syncTask: SyncTask, - private val typingUsersTracker: DefaultTypingUsersTracker, private val networkConnectivityChecker: NetworkConnectivityChecker, private val backgroundDetectionObserver: BackgroundDetectionObserver, private val activeCallHandler: ActiveCallHandler ) : Thread("SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener { private var state: SyncState = SyncState.Idle - private var liveState = MutableLiveData(state) + private var liveState = MutableLiveData(state) private val lock = Object() private val syncScope = CoroutineScope(SupervisorJob()) private val debouncer = Debouncer(createUIHandler()) @@ -231,7 +229,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, return } state = newState - debouncer.debounce("post_state", Runnable { + debouncer.debounce("post_state", { liveState.value = newState }, 150) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt index 4dc54d3b19..fb5e3a5774 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt @@ -25,6 +25,9 @@ import java.io.InputStream */ @WorkerThread fun writeToFile(inputStream: InputStream, outputFile: File) { + // Ensure the parent folder exists, else it will crash + outputFile.parentFile?.mkdirs() + outputFile.outputStream().use { inputStream.copyTo(it) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LruCache.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LruCache.kt new file mode 100644 index 0000000000..0998601db6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LruCache.kt @@ -0,0 +1,24 @@ +/* + * 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.util + +import androidx.collection.LruCache + +@Suppress("NULLABLE_TYPE_PARAMETER_AGAINST_NOT_NULL_TYPE_PARAMETER") +internal inline fun LruCache.getOrPut(key: K, defaultValue: () -> V): V { + return get(key) ?: defaultValue().also { put(key, it) } +} diff --git a/matrix-sdk-android/src/main/res/values-bg/strings_sas.xml b/matrix-sdk-android/src/main/res/values-bg/strings_sas.xml new file mode 100644 index 0000000000..e2ee9faefc --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-bg/strings_sas.xml @@ -0,0 +1,68 @@ + + + + Куче + Котка + Лъв + Кон + Еднорог + Прасе + Слон + Заек + Панда + Петел + Пингвин + Костенурка + Риба + Октопод + Пеперуда + Цвете + Дърво + Кактус + Гъба + Глобус + Луна + Облак + Огън + Банан + Ябълка + Ягода + Царевица + Пица + Торта + Сърце + Усмивка + Робот + Шапка + Очила + Гаечен ключ + Дядо Коледа + Палец нагоре + Чадър + Пясъчен часовник + Часовник + Подарък + Лампа + Книга + Молив + Кламер + Ножици + Катинар + Ключ + Чук + Телефон + Флаг + Влак + Колело + Самолет + Ракета + Трофей + Топка + Китара + Тромпет + Звънец + Котва + Слушалки + Папка + Кабърче + diff --git a/matrix-sdk-android/src/main/res/values-ca/strings_sas.xml b/matrix-sdk-android/src/main/res/values-ca/strings_sas.xml new file mode 100644 index 0000000000..acc0dcbc72 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-ca/strings_sas.xml @@ -0,0 +1,68 @@ + + + + Gos + Gat + Lleó + Cavall + Unicorn + Porc + Elefant + Conill + Panda + Gall + Pingüí + Tortuga + Peix + Pop + Papallona + Flor + Arbre + Cactus + Bolet + Globus terraqüi + Lluna + Núvol + Foc + Plàtan + Poma + Maduixa + Blat de moro + Pizza + Pastís + Cor + Somrient + Robot + Barret + Ulleres + Clau anglesa + Pare Noél + Polzes amunt + Paraigües + Rellotge de sorra + Rellotge + Regal + Bombeta + Llibre + Llapis + Clip + Tisores + Cadenat + Clau + Martell + Telèfon + Bandera + Tren + Bicicleta + Avió + Coet + Trofeu + Pilota + Guitarra + Trompeta + Campana + Àncora + Auriculars + Carpeta + Xinxeta + diff --git a/matrix-sdk-android/src/main/res/values-cs/strings.xml b/matrix-sdk-android/src/main/res/values-cs/strings.xml index ebf7590596..50dea12b09 100644 --- a/matrix-sdk-android/src/main/res/values-cs/strings.xml +++ b/matrix-sdk-android/src/main/res/values-cs/strings.xml @@ -218,4 +218,28 @@ %1$s vstoupili Založili jste diskusi %1$s založil diskusi + Prázdná místnost (byla %s) + + %1$s, %2$s, %3$s a %4$d další + %1$s, %2$s, %3$s a %4$d další + %1$s, %2$s, %3$s a %4$d dalších + + %1$s, %2$s, %3$s a %4$s + %1$s, %2$s a %3$s + 🎉 Účast všech serverů je zakázána! Tuto místnost již nelze použít. + Beze změny. + • Servery shodující se doslovně s IP jsou nyní zakázány. + • Servery shodující se doslovně s IP jsou nyní povoleny. + • Servery shodující se s %s byly odstraněny ze seznamu povolených. + • Servery shodující se s %s jsou nyní povoleny. + • Servery shodující se s %s byly odstraněny ze seznamu zakázaných. + • Servery shodující se s %s jsou nyní zakázány. + Změnili jste ACL serveru pro tuto místnost. + %s změnili ACL serveru pro tuto místnost. + • Server shodující se doslovně s IP je povolen. + • Server shodující se doslovně s IP je zakázán. + • Server shodující se s %s je povolen. + • Server shodující se s %s je zakázán. + Nastavili jste ACL serveru pro tuto místnost. + %s nastavili ACL serveru pro tuto místnost. \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-de/strings.xml b/matrix-sdk-android/src/main/res/values-de/strings.xml index 4c574d578a..bdeeafccb6 100644 --- a/matrix-sdk-android/src/main/res/values-de/strings.xml +++ b/matrix-sdk-android/src/main/res/values-de/strings.xml @@ -56,7 +56,7 @@ E-Mail-Adresse Telefonnummer - %1$s sandte einen Sticker. + %1$s hat einen Sticker gesendet. Einladung von %s Raumeinladung diff --git a/matrix-sdk-android/src/main/res/values-et/strings.xml b/matrix-sdk-android/src/main/res/values-et/strings.xml index 71ee50f075..957c0b9955 100644 --- a/matrix-sdk-android/src/main/res/values-et/strings.xml +++ b/matrix-sdk-android/src/main/res/values-et/strings.xml @@ -213,4 +213,27 @@ %1$s liitus Sina alustasid vestlust %1$s alustas vestlust + Tühi jututuba (oli %s) + + %1$s, %2$s, %3$s ja %4$d muu + %1$s, %2$s, %3$s ja %4$d muud + + %1$s, %2$s, %3$s ja %4$s + %1$s, %2$s ja %3$s + 🎉 Kõikide serverite osalemine on keelatud! Seda jututuba ei saa enam kasutada. + Muudatusi ei ole. + • Nüüd on keelatud serverid, mille ip-aadress vastab mustrile. + • Nüüd on lubatud serverid, mille ip-aadress vastab mustrile. + • Server, mille nimes leidub %s, eemaldati lubatud serverite loendist. + • Nüüd on lubatud serverid, mille nimes leidub %s. + • Server, mille nimes leidub %s eemaldati keeluloendist. + • Keelatud on server, mille nimes leidub %s. + Sina muutsid selle jututoa jaoks serverite pääsuloendit. + %s muutis selle jututoa jaoks serverite pääsuloendit. + Sina kirjeldasid selle jututoa jaoks serverite pääsuloendi. + %s kirjeldas selle jututoa jaoks serverite pääsuloendi. + • Keelatud on serverid, mille ip-aadress vastab mustrile. + • Lubatud on serverid, mille ip-aadress vastab mustrile. + • Lubatud on serverid, mille nimes leidub %s. + • Keelatud on serverid, mille nimes leidub %s. \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-fa/strings.xml b/matrix-sdk-android/src/main/res/values-fa/strings.xml index 11a786f5ac..8f8059067e 100644 --- a/matrix-sdk-android/src/main/res/values-fa/strings.xml +++ b/matrix-sdk-android/src/main/res/values-fa/strings.xml @@ -213,4 +213,9 @@ %1$s پیوست گفت‌وگو را ایجاد کردید %1$s گفت‌وگو را ایجاد کرد + + %1$s، %2$s، %3$s و %4$d نفر دیگر + %1$s، %2$s، %3$s و %4$d نفر دیگر + + %1$s، %2$s و %3$s \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-fi/strings.xml b/matrix-sdk-android/src/main/res/values-fi/strings.xml index fccd22d3b6..1e3788476f 100644 --- a/matrix-sdk-android/src/main/res/values-fi/strings.xml +++ b/matrix-sdk-android/src/main/res/values-fi/strings.xml @@ -1,7 +1,6 @@ - + %1$s lähetti kuvan. - Käyttäjän %s kutsu %1$s kutsui käyttäjän %2$s %1$s kutsui sinut @@ -29,11 +28,9 @@ kaikki. tuntematon (%s). %1$s otti käyttöön osapuolten välisen salauksen (%2$s) - %1$s lähetti VoIP-konferenssipyynnön VoIP-konferenssi alkoi VoIP-konferenssi päättyi - (myös kuva vaihdettiin) %1$s poisti huoneen nimen %1$s poisti huoneen aiheen @@ -42,44 +39,31 @@ %1$s hyväksyi kutsun käyttäjän %2$s puolesta ** Salauksen purku epäonnistui: %s ** Lähettäjän laite ei ole lähettänyt avaimia tähän viestiin. - Viestin lähetys epäonnistui - Kuvan lataaminen epäonnistui - Verkkovirhe Matrix-virhe - Tällä hetkellä ei ole mahdollista liittyä uudelleen tyhjään huoneeseen. - Salattu viesti - Sähköpostiosoite Puhelinnumero - - Takaisinveto epäonnistui %1$s: %2$s - - Kutsu käyttäjältä %s + Kutsu käyttäjältä %s + Huonekutsu %1$s ja %2$s Tyhjä huone - - %1$s lähetti tarran. - %1$s ja yksi muu %1$s ja %2$d muuta - Viesti poistettu %1$s poisti viestin Viesti poistettu [syy: %1$s] %1$s poisti viestin [syy: %2$s] - Alkusynkronointi: \nTuodaan tiliä… Alkusynkronointi: @@ -96,12 +80,9 @@ \nTuodaan yhteisöjä Alkusynkronointi: \nTuodaan tilin tietoja - %s päivitti tämän huoneen. - Lähetetään viestiä… Tyhjennä lähetysjono - %1$s veti takaisin käyttäjän %2$s liittymiskutsun huoneeseen Henkilön %1$s kutsu. Syy: %2$s %1$s kutsui henkilön %2$s. Syy: %3$s @@ -116,28 +97,117 @@ %1$s kumosi kutsun liittyä huoneeseen käyttäjälle %2$s. Syy: %3$s %1$s hyväksyi kutsun liityäkseen huoneeseen %2$s. Syy: %3$s %1$s veti takaisin käyttäjän %2$s kutsun. Syy: %3$s - %1$s lisäsi tälle huoneelle osoitteen %2$s. %1$s lisäsi tälle huoneelle osoitteet %2$s. - %1$s poisti tältä huoneelta osoitteen %2$s. %1$s poisti tältä huoneelta osoitteet %3$s. - %1$s lisäsi tälle huoneelle osoitteen %2$s ja poisti osoitteen %3$s. - %1$s asetti tämän huoneen pääosoitteeksi %2$s. %1$s poisti tämän huoneen pääosoitteen. - %1$s salli vieraiden liittyä huoneeseen. %1$s esti vieraita liittymästä huoneeseen. - %1$s laittoi päälle osapuolten välisen salauksen. %1$s laittoi päälle osapuolisten välisen salauksen (tuntematon algoritmi %2$s). - %s haluaa varmentaa salausavaimesi, mutta asiakasohjelmasi ei tue keskustelun aikana tapahtuvaa avainten varmennusta. Joudut käyttämään perinteistä varmennustapaa. - - + Hyväksyit käyttäjän %1$s kutsun. Syy: %2$s + Peruutit kutsun liittyä huoneeseen käyttäjältä %1$s. Syy: %2$s + Lähetit kutsun liittyä huoneeseen käyttäjälle %1$s. Syy: %2$s + Estit käyttäjän %1$s. Syy: %2$s + Peruutit eston %1$s. Syy: %2$s + Poistit käyttäjän %1$s. Syy: %2$s + Hylkäsit kutsun. Syy: %1$s + Lähdit. Syy: %1$s + %1$s lähti. Syy: %2$s + Poistuit huoneesta. Syy: %1$s + Liityit. Syy: %1$s + %1$s liittyi. Syy: %2$s + Liityit ryhmään. Syy: %1$s + Kutsuit %1$s. Syy: %2$s + Kutsusi. Syy: %1$s + Tyhjä huone (oli %s) + %1$s, %2$s, %3$s ja %4$s + %1$s, %2$s ja %3$s + Mukautettu + Mukautettu (%1$d) + Oletus + Valvoja + Ylläpitäjä + %1$s muutti %2$s sovelmaa + Poistit %1$s sovelman + %1$s poisti %2$s sovelman + Lisäsit %1$s sovelman + %1$s lisäsi %2$s sovelman + Muutit %1$s sovelmaa + Hyväksyit kutsun henkilölle %1$s + Peruutit kutsun henkilöltä %1$s + %1$s peruutti kutsun henkilöltä %2$s + Peruutit henkilön %1$s kutsun liittyä ryhmään + Kutsuit %1$s + %1$s kutsui %2$s + Lähetit henkilölle %1$s kutsun liittyä huoneeseen + Päivitit profiilisi %1$s + Poistit huoneen profiilikuvan + %1$s poisti huoneen profiilikuvan + Poistit huoneen aiheen + Poistit huoneen nimen + Pyysit ryhmäpuhelua + 🎉 Kaikki palvelimet on estetty osallistumasta! Tätä huonetta ei voi enää käyttää. + Ei muutosta. + • Palvelimet jotka %s poistettiin estolistalta. + • Palvelimen haku %s on nyt kielletty. + • Palvelimen haku %s on sallittu. + • Palvelimen haku %s on kielletty. + %1$s on estänyt vieraita liittymästä huoneeseen. + Estit vieraita liittymästä huoneeseen. + Annoit vieraille luvan liittyä huoneeseen. + Annoit vieraille luvan liittyä tänne. + %1$s on antanut vieraille luvan liittyä tänne. + Poistit tämän huoneen pääosoitteen. + Otit käyttöön päästä päähän -salauksen. + Olet estänyt vieraiden liittymisen huoneeseen. + Otit päästä päähän -salauksen käyttöön (tuntematon algoritmi %1$s). + Päivitit tässä. + %s päivitti täällä. + Päivitit tämän huoneen. + Otit päästä päähän -salauksen käyttöön (%1$s) + Teit tulevista viesteistä näkyviä käyttäjälle %1$s + %1$s teki tulevista viesteistä näkyviä käyttäjälle %2$s + Teit tulevan huonehistorian näkyväksi %1$s + Lopetit puhelun. + Vastasit puheluun. + Lähetit tietoja puhelun valmistelemiseksi. + %s lähetti tietoja puhelun valmistelemiseksi. + Aloitit äänipuhelun. + Aloitit videopuhelun. + Vaihdoit huoneen nimeksi: %1$s + Vaihdoit huoneen profiilikuvaa + %1$s muutti huoneen profiilikuvaa + Vaihdoit aiheen: %1$s + Poistit nimimerkkisi (se oli %1$s) + Vaihdoit nimimerkkisi %1$s nimeen %2$s + Asetit nimimerkiksesi %1$s + Kutsusi + Vaihdoit profiilikuvaasi + Peruutit %1$sn kutsun + Estit %1$s + Poistit eston %1$s + Poistit %1$s + Hylkäsit kutsun + Poistuit huoneesta + %1$s poistui huoneesta + Poistuit huoneesta + Liityit + %1$s liittyi + Liityit huoneeseen + Kutsuit %1$s + Loit keskustelun + %1$s loi keskustelun + Loit huoneen + %1$s loi huoneen + Lähetit tarran. + Lähetit kuvan. + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-fr/strings.xml b/matrix-sdk-android/src/main/res/values-fr/strings.xml index 9c5fa7d3b1..f49c54a8ba 100644 --- a/matrix-sdk-android/src/main/res/values-fr/strings.xml +++ b/matrix-sdk-android/src/main/res/values-fr/strings.xml @@ -213,4 +213,27 @@ Vous avez exclus %1$s Vous avez révoqué l\'exclusion de %1$s Vous avez expulsé %1$s. Raison : %2$s + Salon vide (était %s) + + %1$s, %2$s, %3$s et %4$d autre + %1$s, %2$s, %3$s et %4$d autres + + %1$s, %2$s, %3$s et %4$s + %1$s, %2$s et %3$s + 🎉 Tous les serveurs sont interdits de participer ! Ce salon ne peut plus être utilisé. + Aucun changement. + • Les serveurs correspondant à des IP littérales sont maintenant interdits. + • Les serveurs correspondant à %s sont interdits. + • Les serveurs correspondants à des IP littérales sont interdites. + • Les serveurs correspondants à des IP littérales sont autorisés. + • Les serveurs correspondants à des IP littérales sont maintenant autorisées. + • Les serveurs correspondant à %s sont supprimés de la liste autorisée. + • les serveur correspondant à %s sont maintenant autorisés. + • Les serveurs correspondant à %s étaient supprimés de la liste des interdits. + • Les serveurs correspondant à %s sont maintenant interdits. + Vous avez changé les droits ACL du serveur pour ce salon. + %s a changé les droits ACL du serveur pour ce salon. + • Les serveurs correspondant à %s sont autorisés. + Vous avez paramétré les ACL pour ce salon. + %s paramètre les autorisations étendues (ACL) du serveur pour ce salon. \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-it/strings.xml b/matrix-sdk-android/src/main/res/values-it/strings.xml index 5eab8c57df..c1a5cf85cb 100644 --- a/matrix-sdk-android/src/main/res/values-it/strings.xml +++ b/matrix-sdk-android/src/main/res/values-it/strings.xml @@ -214,4 +214,27 @@ %1$s è entrato Hai creato la discussione %1$s ha creato la discussione + Stanza vuota (era %s) + + %1$s, %2$s, %3$s e %4$d altro + %1$s, %2$s, %3$s e altri %4$d + + %1$s, %2$s, %3$s e %4$s + %1$s, %2$s e %3$s + 🎉 Tutti i server sono banditi dalla partecipazione! Questa stanza non può più essere usata. + Nessuna modifica. + • I server che corrispondono a IP letterali ora sono banditi. + • I server che corrispondono a IP letterali ora sono permessi. + • I server che corrispondono a %s sono stati rimossi dalla lista dei consentiti. + • I server che corrispondono a %s ora sono permessi. + • I server che corrispondono a %s sono stati rimossi dalla lista di ban. + • I server che corrispondono a %s ora sono banditi. + Hai cambiato le ACL del server per questa stanza. + %s ha cambiato le ACL del server per questa stanza. + • I server che corrispondono a IP letterali sono banditi. + • I server che corrispondono a IP letterali sono permessi. + • I server che corrispondono a %s sono permessi. + • I server che corrispondono a %s sono banditi. + Hai impostato le ACL del server per questa stanza. + %s ha impostato le ACL del server per questa stanza. \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml b/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml index ed9f91cdb3..e6c93cb55c 100644 --- a/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml +++ b/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml @@ -221,4 +221,27 @@ %1$s entrou Você criou a sala %1$s criou a sala + Sala vazia (era %s) + + %1$s, %2$s, %3$s e %4$d outro + %1$s, %2$s, %3$s e %4$d outros + + %1$s, %2$s, %3$s e %4$s + %1$s, %2$s e %3$s + 🎉 Todos os servidores estão proibidos de participar! Esta sala não pode mais ser usada. + Nenhuma alteração. + • Servidores correspondentes aos IP literais agora estão banidos. + • Servidores correspondentes aos IP literais agora estão permitidos. + • Servidores correspondentes à %s foram removidos da lista de permitidos. + • Servidores correspondentes à %s agora são permitidos. + • Servidores correspondente à %s foram removidos da lista de banidos. + • Servidores correspondentes à %s foram banidos. + Você alterou a lista de controle de acesso (ACL) do servidor para esta sala. + %s alterou a lista de controle de acesso (ACL) do servidor para esta sala. + • Servidores correspondentes aos IP literais estão banidos. + • Servidores correspondentes aos IP literais estão permitidos. + • Servidores correspondentes à %s estão permitidos. + • Servidores correspondentes à %s estão banidos. + Você definiu a lista de controle de acesso (ACL) do servidor para esta sala. + %s definiu a lista de controle de acesso (ACL) do servidor para esta sala. \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-ru/strings.xml b/matrix-sdk-android/src/main/res/values-ru/strings.xml index ef9cda1dc2..5ef5a4f447 100644 --- a/matrix-sdk-android/src/main/res/values-ru/strings.xml +++ b/matrix-sdk-android/src/main/res/values-ru/strings.xml @@ -225,6 +225,31 @@ %1$s вошел(ла) Вы создали обсуждение %1$s создал(а) обсуждение - Вы обновили. - %s обновлена. + Вы обновили эту комнату. + %s обновил(а) эту комнату. + + %1$s, %2$s, %3$s и %4$d другой + %1$s, %2$s, %3$s и %4$d других + %1$s, %2$s, %3$s и %4$d другие + %1$s, %2$s, %3$s и %4$d другие + + %1$s, %2$s, %3$s и %4$s + %1$s, %2$s и %3$s + 🎉 Всем серверам запрещено участвовать! Эта комната больше не может быть использована. + Без изменений. + Пустая комната (была %s) + • Соответствующий сервер %s заблокирован. + • Сервер, соответствующий буквальным IP-адресам, теперь запрещён. + • Сервер, соответствующий буквальным IP-адресам, теперь разрешён. + • Сервер, соответствующий %s, теперь запрещён. + • Сервер, соответствующий %s, теперь разрешён. + • Сервер, соответствующий %s, был удалён из списка блокировки. + • Сервер, соответствующий буквальным IP-адресам, запрещён. + • Сервер, соответствующий буквальным IP-адресам, разрешён. + • Сервер, соответствующий %s, разрешён. + • Сервер, соответствующий %s, был удалён из разрешённого списка. + Вы изменили права доступа сервера (ACL) для этой комнаты. + %s изменил права доступа сервера (ACL) для этой комнаты. + Вы настроили права доступа сервера (ACL) для этой комнаты. + %s устанавливает права доступа сервера (ACL) для этой комнаты. \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-sq/strings.xml b/matrix-sdk-android/src/main/res/values-sq/strings.xml index 4055b35025..58ba8877bb 100644 --- a/matrix-sdk-android/src/main/res/values-sq/strings.xml +++ b/matrix-sdk-android/src/main/res/values-sq/strings.xml @@ -213,4 +213,23 @@ %1$s erdhi Krijuat diskutimin %1$s krijoi diskutimin + Dhomë e zbrazët (was %s) + + %1$s, %2$s, %3$s dhe %4$d tjetër + %1$s, %2$s, %3$s dhe %4$d të tjerë + + %1$s, %2$s, %3$s dhe %4$s + %1$s, %2$s dhe %3$s + 🎉 U është penguar pjesëmarrja krejt shërbyesve! Kjo dhomë s’mund të përdoret më. + Pa ndryshim. + Ndryshuat ACL-ra shërbyesi për këtë dhomë. + %s ndryshoi ACL-ra shërbyesi për këtë dhomë. + Ujdisët ACL-ra shërbyesi për këtë dhomë. + %s ujdisi ACL-ra shërbyesi për këtë dhomë. + • Shërbyes që kanë përputhje me %s u hoqën nga lista e të lejuarve. + • Shërbyesit që kanë përputhje me %s tani janë të lejuar. + • Shërbyesit që kanë përputhje me %s u hoqën nga lista e ndalimeve. + • Shërbyesit që kanë përputhje me %s tani janë të ndaluar. + • Shërbyesit që kanë përputhje me %s janë të ndaluar. + • Shërbyesit që kanë përputhje me %s janë të ndaluar. \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-uk/strings.xml b/matrix-sdk-android/src/main/res/values-uk/strings.xml index eb5071f190..2477487379 100644 --- a/matrix-sdk-android/src/main/res/values-uk/strings.xml +++ b/matrix-sdk-android/src/main/res/values-uk/strings.xml @@ -1,39 +1,34 @@ - + %1$s: %2$s %1$s надіслав(ла) зображення. - %s запрошення %1$s запросив(ла) %2$s Зашифроване повідомлення - Запрошення від %s Запрошення до кімнати %1$s і %2$s Порожня кімната - - %1$s надіслав(ла) наліпку. - %1$s запросив(ла) Вас - %1$s приєднався(лась) + %1$s приєднується %1$s покинув(ла) %1$s відхилив(ла) запрошення %1$s копнув(ла) %2$s %1$s розблокував(ла) %2$s %1$s заблокував(ла) %2$s %1$s відкликав(ла) запрошення для %2$s - %1$s змінив(ла) свій аватар - %1$s встановив(ла) собі ім’я %2$s - %1$s змінив(ла) своє ім’я з %2$s на %3$s + %1$s змінює свій аватар + %1$s встановлюють собі назву %2$s + %1$s змінює своє ім’я з %2$s на %3$s %1$s прибрав(ла) своє ім’я (%2$s) - %1$s змінив(ла) тему на: %2$s + %1$s змінює тему на: %2$s %1$s змінив(ла) назву кімнати на: %2$s %s розпочав(ла) відеодзвінок. %s розпочав(ла) голосовий дзвінок. %s відповів(ла) на дзвінок. - %s завершив(ла) дзвінок. + %s завершує дзвінок. %1$s зробив(ла) майбутню історію кімнати видимою для %2$s усіх співрозмовників, з моменту їх запрошення. усіх співрозмовників, з моменту їх приєднання. @@ -41,44 +36,33 @@ будь-кого. невідомо (%s). %1$s увімкнув(ла) наскрізне шифрування (%2$s) - %1$s запросив(ла) VoIP конференцію VoIP конференція розпочалась VoIP конференція завершилась - (аватар також змінено) %1$s прибрав(ла) назву кімнати %1$s прибрав(ла) тему кімнати %1$s оновив(ла) свій профіль %2$s %1$s надіслав(ла) запрошення %2$s приєднатися до кімнати %1$s прийняв(ла) запрошення у %2$s - ** Неможливо розшифрувати: %s ** Пристрій відправника не надіслав нам ключ для цього повідомлення. - Неможливо відредагувати Не вдалося надіслати повідомлення - Не вдалося завантажити зображення - Помилка мережі Помилка Matrix - Наразі неможливо переприєднатися до порожньої кімнати. - Адреса електронної пошти Номер телефону - %1$s та 1 інший %1$s та %2$d інші %1$s та %2$d інших - + - %s вдосконалили цю кімнату. - Повідомлення видалено %1$s видалили повідомлення Повідомлення видалено [причина: %1$s] - + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml index b3de5910a5..08050b400d 100644 --- a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml +++ b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml @@ -208,4 +208,26 @@ %1$s 已加入 您已建立此討論 %1$s 已建立此討論 + 空的聊天室(曾為 %s) + + %1$s, %2$s, %3$s 與 %4$d 個其他 + + %1$s, %2$s, %3$s 與 %4$s + %1$s, %2$s 與 %3$s + 🎉 禁止所有伺服器參與!無法再使用此聊天室。 + 無變更。 + • 禁止伺服器符合 IP 文字。 + • 允許伺服器符合 IP 文字。 + • 伺服器符合 %s 已從允許清單中移除。 + • 允許伺服器符合 %s。 + • 伺服器符合 %s 已從禁止清單中移除。 + • 現在禁止伺服器符合 %s。 + 您為此聊天室變更了伺服器 ACL。 + %s 為此聊天是變更了伺服器 ACL。 + • 禁止伺服器符合 IP 文字。 + • 允許伺服器符合 IP 文字。 + • 已允許伺服器符合 %s。 + • 已禁止伺服器符合 %s。 + 您為此聊天是設定了伺服器 ACL。 + %s 為此聊天是設定了伺服器 ACL。 \ No newline at end of file diff --git a/multipicker/build.gradle b/multipicker/build.gradle index 7c29a5539f..c58c4586b2 100644 --- a/multipicker/build.gradle +++ b/multipicker/build.gradle @@ -16,7 +16,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-parcelize' android { compileSdkVersion 29 diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index c7a477cd42..ff5f69db5c 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -109,9 +109,6 @@ import retrofit2\.adapter\.rxjava\.HttpException ### This is generally not necessary, no need to reset the padding if there is no drawable setCompoundDrawablePadding\(0\) -### Deprecated use class form SDK API 26 -ButterKnife\.findById\( - # Change thread with Rx # DISABLED #runOnUiThread @@ -164,7 +161,7 @@ Formatter\.formatShortFileSize===1 # android\.text\.TextUtils ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt -enum class===85 +enum class===86 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 @@ -175,3 +172,6 @@ getSystemService\(Context ### Use DefaultSharedPreferences.getInstance() instead for better performance PreferenceManager\.getDefaultSharedPreferences==2 + +### Use ViewBindings +# findViewById diff --git a/tools/templates/ElementFeature/root/src/app_package/Fragment.kt.ftl b/tools/templates/ElementFeature/root/src/app_package/Fragment.kt.ftl index 7e6eac65c8..38f8132d24 100644 --- a/tools/templates/ElementFeature/root/src/app_package/Fragment.kt.ftl +++ b/tools/templates/ElementFeature/root/src/app_package/Fragment.kt.ftl @@ -3,7 +3,7 @@ package ${escapeKotlinIdentifiers(packageName)} import android.os.Bundle <#if createFragmentArgs> import android.os.Parcelable -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize import com.airbnb.mvrx.args import android.view.View @@ -36,8 +36,8 @@ class ${fragmentClass} @Inject constructor( } override fun onDestroyView() { - super.onDestroyView() // Clear your view, unsubscribe... + super.onDestroyView() } override fun invalidate() = withState(viewModel) { state -> diff --git a/vector/build.gradle b/vector/build.gradle index 69f8daf47b..496582e41c 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -3,21 +3,17 @@ import com.android.build.OutputFile apply plugin: 'com.android.application' apply plugin: 'com.google.android.gms.oss-licenses-plugin' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-kapt' kapt { correctErrorTypes = true } -androidExtensions { - experimental = true -} - // Note: 2 digits max for each value ext.versionMajor = 1 ext.versionMinor = 0 -ext.versionPatch = 12 +ext.versionPatch = 14 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' @@ -283,6 +279,10 @@ android { java.srcDirs += "src/sharedTest/java" } } + + buildFeatures { + viewBinding true + } } dependencies { @@ -390,10 +390,6 @@ dependencies { implementation 'com.otaliastudios:autocomplete:1.1.0' - // Butterknife - implementation 'com.jakewharton:butterknife:10.2.0' - kapt 'com.jakewharton:butterknife-compiler:10.2.0' - // Shake detection implementation 'com.squareup:seismic:1.0.2' @@ -414,6 +410,9 @@ dependencies { // Badge for compatibility implementation 'me.leolin:ShortcutBadger:1.1.22@aar' + // Chat effects + implementation 'nl.dionsegijn:konfetti:1.2.5' + implementation 'com.github.jetradarmobile:android-snowfall:1.2.0' // DI implementation "com.google.dagger:dagger:$daggerVersion" kapt "com.google.dagger:dagger-compiler:$daggerVersion" @@ -444,6 +443,10 @@ dependencies { implementation 'com.google.zxing:core:3.3.3' implementation 'me.dm7.barcodescanner:zxing:1.9.13' + // Emoji Keyboard + implementation 'com.vanniktech:emoji-material:0.7.0' + implementation 'com.vanniktech:emoji-google:0.7.0' + // TESTS testImplementation 'junit:junit:4.13' testImplementation "org.amshove.kluent:kluent-android:$kluent_version" diff --git a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt index 6b30700116..cc4724e8f3 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt @@ -76,7 +76,9 @@ class UiAllScreensSanityTest { private val uiTestBase = UiTestBase() - // Last passing: 2020-11-09 + // Last passing: + // 2020-11-09 + // 2020-12-16 After ViewBinding huge change @Test fun allScreensTest() { // Create an account diff --git a/vector/src/debug/java/im/vector/app/features/debug/DebugMaterialThemeActivity.kt b/vector/src/debug/java/im/vector/app/features/debug/DebugMaterialThemeActivity.kt index 6c4bb925dd..8df1feab1e 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/DebugMaterialThemeActivity.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/DebugMaterialThemeActivity.kt @@ -24,26 +24,27 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.snackbar.Snackbar import im.vector.app.R import im.vector.app.core.utils.toast -import kotlinx.android.synthetic.debug.activity_test_material_theme.* +import im.vector.app.databinding.ActivityTestMaterialThemeBinding // Rendering is not the same with VectorBaseActivity abstract class DebugMaterialThemeActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_test_material_theme) + val views = ActivityTestMaterialThemeBinding.inflate(layoutInflater) + setContentView(views.root) - debugShowSnackbar.setOnClickListener { - Snackbar.make(debugMaterialCoordinator, "Snackbar!", Snackbar.LENGTH_SHORT) + views.debugShowSnackbar.setOnClickListener { + Snackbar.make(views.coordinatorLayout, "Snackbar!", Snackbar.LENGTH_SHORT) .setAction("Action") { } .show() } - debugShowToast.setOnClickListener { + views.debugShowToast.setOnClickListener { toast("Toast") } - debugShowDialog.setOnClickListener { + views.debugShowDialog.setOnClickListener { AlertDialog.Builder(this) .setMessage("Dialog content") .setIcon(R.drawable.ic_settings_x) @@ -53,7 +54,7 @@ abstract class DebugMaterialThemeActivity : AppCompatActivity() { .show() } - debugShowBottomSheet.setOnClickListener { + views.debugShowBottomSheet.setOnClickListener { BottomSheetDialogFragment().show(supportFragmentManager, "TAG") } } diff --git a/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt b/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt index 9cca462d1a..8699b238ec 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt @@ -24,7 +24,6 @@ import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.Person import androidx.core.content.getSystemService -import butterknife.OnClick import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ScreenComponent @@ -35,16 +34,17 @@ import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA import im.vector.app.core.utils.allGranted import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.toast +import im.vector.app.databinding.ActivityDebugMenuBinding import im.vector.app.features.debug.sas.DebugSasEmojiActivity import im.vector.app.features.qrcode.QrCodeScannerActivity import org.matrix.android.sdk.internal.crypto.verification.qrcode.toQrCodeData -import kotlinx.android.synthetic.debug.activity_debug_menu.* + import timber.log.Timber import javax.inject.Inject -class DebugMenuActivity : VectorBaseActivity() { +class DebugMenuActivity : VectorBaseActivity() { - override fun getLayoutRes() = R.layout.activity_debug_menu + override fun getBinding() = ActivityDebugMenuBinding.inflate(layoutInflater) @Inject lateinit var activeSessionHolder: ActiveSessionHolder @@ -66,24 +66,32 @@ class DebugMenuActivity : VectorBaseActivity() { val string = buffer.toString(Charsets.ISO_8859_1) renderQrCode(string) + setupViews() + } + + private fun setupViews() { + views.debugTestTextViewLink.setOnClickListener { testTextViewLink() } + views.debugShowSasEmoji.setOnClickListener { showSasEmoji() } + views.debugTestNotification.setOnClickListener { testNotification() } + views.debugTestMaterialThemeLight.setOnClickListener { testMaterialThemeLight() } + views.debugTestMaterialThemeDark.setOnClickListener { testMaterialThemeDark() } + views.debugTestCrash.setOnClickListener { testCrash() } + views.debugScanQrCode.setOnClickListener { scanQRCode() } } private fun renderQrCode(text: String) { - debug_qr_code.setData(text) + views.debugQrCode.setData(text) } - @OnClick(R.id.debug_test_text_view_link) - fun testTextViewLink() { + private fun testTextViewLink() { startActivity(Intent(this, TestLinkifyActivity::class.java)) } - @OnClick(R.id.debug_show_sas_emoji) - fun showSasEmoji() { + private fun showSasEmoji() { startActivity(Intent(this, DebugSasEmojiActivity::class.java)) } - @OnClick(R.id.debug_test_notification) - fun testNotification() { + private fun testNotification() { val notificationManager = getSystemService()!! // Create channel first @@ -166,23 +174,19 @@ class DebugMenuActivity : VectorBaseActivity() { ) } - @OnClick(R.id.debug_test_material_theme_light) - fun testMaterialThemeLight() { + private fun testMaterialThemeLight() { startActivity(Intent(this, DebugMaterialThemeLightActivity::class.java)) } - @OnClick(R.id.debug_test_material_theme_dark) - fun testMaterialThemeDark() { + private fun testMaterialThemeDark() { startActivity(Intent(this, DebugMaterialThemeDarkActivity::class.java)) } - @OnClick(R.id.debug_test_crash) - fun testCrash() { + private fun testCrash() { throw RuntimeException("Application crashed from user demand") } - @OnClick(R.id.debug_scan_qr_code) - fun scanQRCode() { + private fun scanQRCode() { if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) { doScanQRCode() } diff --git a/vector/src/debug/java/im/vector/app/features/debug/TestLinkifyActivity.kt b/vector/src/debug/java/im/vector/app/features/debug/TestLinkifyActivity.kt index fd28aabd49..88e55d6760 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/TestLinkifyActivity.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/TestLinkifyActivity.kt @@ -19,28 +19,18 @@ package im.vector.app.features.debug import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup -import android.widget.LinearLayout -import android.widget.TextView import androidx.appcompat.app.AppCompatActivity -import androidx.coordinatorlayout.widget.CoordinatorLayout -import butterknife.BindView -import butterknife.ButterKnife import im.vector.app.R +import im.vector.app.databinding.ActivityTestLinkifyBinding +import im.vector.app.databinding.ItemTestLinkifyBinding class TestLinkifyActivity : AppCompatActivity() { - @BindView(R.id.test_linkify_content_view) - lateinit var scrollContent: LinearLayout - - @BindView(R.id.test_linkify_coordinator) - lateinit var coordinatorLayout: CoordinatorLayout - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_test_linkify) - ButterKnife.bind(this) - - scrollContent.removeAllViews() + val views = ActivityTestLinkifyBinding.inflate(layoutInflater) + setContentView(views.root) + views.testLinkifyContentView.removeAllViews() listOf( "https://www.html5rocks.com/en/tutorials/webrtc/basics/ |", @@ -89,43 +79,42 @@ class TestLinkifyActivity : AppCompatActivity() { ) .forEach { textContent -> val item = LayoutInflater.from(this) - .inflate(R.layout.item_test_linkify, scrollContent, false) - - item.findViewById(R.id.test_linkify_auto_text) - ?.apply { - text = textContent - /* TODO Use BetterLinkMovementMethod when the other PR is merged - movementMethod = MatrixLinkMovementMethod(object : MockMessageAdapterActionListener() { - override fun onURLClick(uri: Uri?) { - Snackbar.make(coordinatorLayout, "URI Clicked: $uri", Snackbar.LENGTH_LONG) - .setAction("open") { - openUrlInExternalBrowser(this@TestLinkifyActivity, uri) - } - .show() - } - }) - */ + .inflate(R.layout.item_test_linkify, views.testLinkifyContentView, false) + val subViews = ItemTestLinkifyBinding.bind(item) + subViews.testLinkifyAutoText.apply { + text = textContent + /* TODO Use BetterLinkMovementMethod when the other PR is merged + movementMethod = MatrixLinkMovementMethod(object : MockMessageAdapterActionListener() { + override fun onURLClick(uri: Uri?) { + Snackbar.make(coordinatorLayout, "URI Clicked: $uri", Snackbar.LENGTH_LONG) + .setAction("open") { + openUrlInExternalBrowser(this@TestLinkifyActivity, uri) + } + .show() } + }) + */ + } - item.findViewById(R.id.test_linkify_custom_text) - ?.apply { - text = textContent - /* TODO Use BetterLinkMovementMethod when the other PR is merged - movementMethod = MatrixLinkMovementMethod(object : MockMessageAdapterActionListener() { - override fun onURLClick(uri: Uri?) { - Snackbar.make(coordinatorLayout, "URI Clicked: $uri", Snackbar.LENGTH_LONG) - .setAction("open") { - openUrlInExternalBrowser(this@TestLinkifyActivity, uri) - } - .show() - } - }) - */ - - // TODO Call VectorLinkify.addLinks(text) + subViews.testLinkifyCustomText.apply { + text = textContent + /* TODO Use BetterLinkMovementMethod when the other PR is merged + movementMethod = MatrixLinkMovementMethod(object : MockMessageAdapterActionListener() { + override fun onURLClick(uri: Uri?) { + Snackbar.make(coordinatorLayout, "URI Clicked: $uri", Snackbar.LENGTH_LONG) + .setAction("open") { + openUrlInExternalBrowser(this@TestLinkifyActivity, uri) + } + .show() } + }) + */ - scrollContent.addView(item, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)) + // TODO Call VectorLinkify.addLinks(text) + } + + views.testLinkifyContentView + .addView(item, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)) } } } diff --git a/vector/src/debug/java/im/vector/app/features/debug/sas/DebugSasEmojiActivity.kt b/vector/src/debug/java/im/vector/app/features/debug/sas/DebugSasEmojiActivity.kt index 869058eff6..4d7005aca8 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/sas/DebugSasEmojiActivity.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/sas/DebugSasEmojiActivity.kt @@ -18,24 +18,26 @@ package im.vector.app.features.debug.sas import android.os.Bundle import androidx.appcompat.app.AppCompatActivity -import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith +import im.vector.app.databinding.FragmentGenericRecyclerBinding import org.matrix.android.sdk.api.crypto.getAllVerificationEmojis -import kotlinx.android.synthetic.main.fragment_generic_recycler.* class DebugSasEmojiActivity : AppCompatActivity() { + private lateinit var views: FragmentGenericRecyclerBinding + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.fragment_generic_recycler) + views = FragmentGenericRecyclerBinding.inflate(layoutInflater) + setContentView(views.root) val controller = SasEmojiController() - genericRecyclerView.configureWith(controller) + views.genericRecyclerView.configureWith(controller) controller.setData(SasState(getAllVerificationEmojis())) } override fun onDestroy() { - genericRecyclerView.cleanup() + views.genericRecyclerView.cleanup() super.onDestroy() } } diff --git a/vector/src/debug/res/layout/activity_debug_menu.xml b/vector/src/debug/res/layout/activity_debug_menu.xml index 9a95085a07..6a41488987 100644 --- a/vector/src/debug/res/layout/activity_debug_menu.xml +++ b/vector/src/debug/res/layout/activity_debug_menu.xml @@ -1,6 +1,7 @@ - + - - + @@ -190,15 +192,25 @@ - + + + + + diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index ac80b0d691..17d79e3655 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -312,11 +312,6 @@ SOFTWARE.
Copyright (c) 2017 -
  • - Butterknife -
    - Copyright 2013 Jake Wharton -
  • seismic
    @@ -347,11 +342,6 @@ SOFTWARE.
    Copyright 2017 Gabriel Ittner.
  • -
  • - Android-multipicker-library -
    - Copyright 2018 Kumar Bibek -
  • htmlcompressor
    @@ -390,6 +380,16 @@ SOFTWARE.
    Copyright 2018, Aleksandr Nikiforov
  • +
  • + Emoji +
    + Copyright (C) 2016 - Niklas Baudy, Ruben Gees, Mario Đanić and contributors +
  • +
  • + JetradarMobile / android-snowfall +
    + Copyright 2016 JetRadar +
  •  Apache License
    @@ -576,5 +576,14 @@ Apache License
         
     
    +
    +    ISC License
    +    
  • + DanielMartinus / Konfetti +
    + Copyright (c) 2017 Dion Segijn +
  • +
    + diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index b9ab23ad7d..1a7fe35745 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -36,6 +36,8 @@ import com.airbnb.epoxy.EpoxyAsyncUtil import com.airbnb.epoxy.EpoxyController import com.facebook.stetho.Stetho import com.gabrielittner.threetenbp.LazyThreeTen +import com.vanniktech.emoji.EmojiManager +import com.vanniktech.emoji.google.GoogleEmojiProvider import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.DaggerVectorComponent import im.vector.app.core.di.HasVectorInjector @@ -184,6 +186,8 @@ class VectorApplication : addAction(Intent.ACTION_SCREEN_OFF) addAction(Intent.ACTION_SCREEN_ON) }) + + EmojiManager.install(GoogleEmojiProvider()) } private fun enableStrictModeIfNeeded() { diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index 188ca32559..87ab875746 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -64,7 +64,6 @@ import im.vector.app.features.login.LoginResetPasswordSuccessFragment import im.vector.app.features.login.LoginServerSelectionFragment import im.vector.app.features.login.LoginServerUrlFormFragment import im.vector.app.features.login.LoginSignUpSignInSelectionFragment -import im.vector.app.features.login.LoginSignUpSignInSsoFragment import im.vector.app.features.login.LoginSplashFragment import im.vector.app.features.login.LoginWaitForEmailFragment import im.vector.app.features.login.LoginWebFragment @@ -230,11 +229,6 @@ interface FragmentModule { @FragmentKey(LoginSignUpSignInSelectionFragment::class) fun bindLoginSignUpSignInSelectionFragment(fragment: LoginSignUpSignInSelectionFragment): Fragment - @Binds - @IntoMap - @FragmentKey(LoginSignUpSignInSsoFragment::class) - fun bindLoginSignUpSignInSsoFragment(fragment: LoginSignUpSignInSsoFragment): Fragment - @Binds @IntoMap @FragmentKey(LoginSplashFragment::class) diff --git a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt index 69c0fab9a8..fb8b1eac07 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt @@ -59,6 +59,7 @@ import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.ui.UiStateRepository import org.matrix.android.sdk.api.Matrix 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.session.Session import javax.inject.Singleton @@ -127,6 +128,8 @@ interface VectorComponent { fun rawService(): RawService + fun homeServerHistoryService(): HomeServerHistoryService + fun bugReporter(): BugReporter fun vectorUncaughtExceptionHandler(): VectorUncaughtExceptionHandler diff --git a/vector/src/main/java/im/vector/app/core/di/VectorModule.kt b/vector/src/main/java/im/vector/app/core/di/VectorModule.kt index 1d7cd33241..77cad0ae73 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorModule.kt @@ -33,6 +33,7 @@ import im.vector.app.features.ui.SharedPreferencesUiStateRepository import im.vector.app.features.ui.UiStateRepository import org.matrix.android.sdk.api.Matrix 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.raw.RawService import org.matrix.android.sdk.api.session.Session @@ -85,6 +86,12 @@ abstract class VectorModule { fun providesRawService(matrix: Matrix): RawService { return matrix.rawService() } + + @Provides + @JvmStatic + fun providesHomeServerHistoryService(matrix: Matrix): HomeServerHistoryService { + return matrix.homeServerHistoryService() + } } @Binds diff --git a/vector/src/main/java/im/vector/app/core/dialogs/ConfirmationDialogBuilder.kt b/vector/src/main/java/im/vector/app/core/dialogs/ConfirmationDialogBuilder.kt index 0d9fb9d93d..ed429b30c6 100644 --- a/vector/src/main/java/im/vector/app/core/dialogs/ConfirmationDialogBuilder.kt +++ b/vector/src/main/java/im/vector/app/core/dialogs/ConfirmationDialogBuilder.kt @@ -21,7 +21,7 @@ import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import im.vector.app.R -import kotlinx.android.synthetic.main.dialog_confirmation_with_reason.view.* +import im.vector.app.databinding.DialogConfirmationWithReasonBinding object ConfirmationDialogBuilder { @@ -33,25 +33,26 @@ object ConfirmationDialogBuilder { @StringRes reasonHintRes: Int, confirmation: (String?) -> Unit) { val layout = activity.layoutInflater.inflate(R.layout.dialog_confirmation_with_reason, null) - layout.dialogConfirmationText.setText(confirmationRes) + val views = DialogConfirmationWithReasonBinding.bind(layout) + views.dialogConfirmationText.setText(confirmationRes) - layout.dialogReasonCheck.isVisible = askForReason - layout.dialogReasonTextInputLayout.isVisible = askForReason + views.dialogReasonCheck.isVisible = askForReason + views.dialogReasonTextInputLayout.isVisible = askForReason - layout.dialogReasonCheck.setOnCheckedChangeListener { _, isChecked -> - layout.dialogReasonTextInputLayout.isEnabled = isChecked + views.dialogReasonCheck.setOnCheckedChangeListener { _, isChecked -> + views.dialogReasonTextInputLayout.isEnabled = isChecked } if (askForReason && reasonHintRes != 0) { - layout.dialogReasonInput.setHint(reasonHintRes) + views.dialogReasonInput.setHint(reasonHintRes) } AlertDialog.Builder(activity) .setTitle(titleRes) .setView(layout) .setPositiveButton(positiveRes) { _, _ -> - val reason = layout.dialogReasonInput.text.toString() + val reason = views.dialogReasonInput.text.toString() .takeIf { askForReason } - ?.takeIf { layout.dialogReasonCheck.isChecked } + ?.takeIf { views.dialogReasonCheck.isChecked } ?.takeIf { it.isNotBlank() } confirmation(reason) } diff --git a/vector/src/main/java/im/vector/app/core/dialogs/ExportKeysDialog.kt b/vector/src/main/java/im/vector/app/core/dialogs/ExportKeysDialog.kt index b4087d5ce1..e137eb1b70 100644 --- a/vector/src/main/java/im/vector/app/core/dialogs/ExportKeysDialog.kt +++ b/vector/src/main/java/im/vector/app/core/dialogs/ExportKeysDialog.kt @@ -18,14 +18,11 @@ package im.vector.app.core.dialogs import android.app.Activity import android.text.Editable -import android.widget.Button -import android.widget.ImageView import androidx.appcompat.app.AlertDialog -import com.google.android.material.textfield.TextInputEditText -import com.google.android.material.textfield.TextInputLayout import im.vector.app.R import im.vector.app.core.extensions.showPassword import im.vector.app.core.platform.SimpleTextWatcher +import im.vector.app.databinding.DialogExportE2eKeysBinding class ExportKeysDialog { @@ -33,48 +30,44 @@ class ExportKeysDialog { fun show(activity: Activity, exportKeyDialogListener: ExportKeyDialogListener) { val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_export_e2e_keys, null) + val views = DialogExportE2eKeysBinding.bind(dialogLayout) val builder = AlertDialog.Builder(activity) .setTitle(R.string.encryption_export_room_keys) .setView(dialogLayout) - val passPhrase1EditText = dialogLayout.findViewById(R.id.exportDialogEt) - val passPhrase2EditText = dialogLayout.findViewById(R.id.exportDialogEtConfirm) - val passPhrase2Til = dialogLayout.findViewById(R.id.exportDialogTilConfirm) - val exportButton = dialogLayout.findViewById