diff --git a/attachment-viewer/.gitignore b/attachment-viewer/.gitignore
new file mode 100644
index 0000000000..42afabfd2a
--- /dev/null
+++ b/attachment-viewer/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle
new file mode 100644
index 0000000000..7fcda7a742
--- /dev/null
+++ b/attachment-viewer/build.gradle
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+
+buildscript {
+ repositories {
+ maven {
+ url 'https://jitpack.io'
+ content {
+ // PhotoView
+ includeGroupByRegex 'com\\.github\\.chrisbanes'
+ }
+ }
+ jcenter()
+ }
+
+}
+
+android {
+ compileSdkVersion 29
+ buildToolsVersion "29.0.3"
+
+ defaultConfig {
+ minSdkVersion 21
+ targetSdkVersion 29
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+}
+
+dependencies {
+// implementation 'com.github.MikeOrtiz:TouchImageView:3.0.2'
+ implementation 'com.github.chrisbanes:PhotoView:2.0.0'
+ implementation "com.github.bumptech.glide:glide:4.10.0"
+
+ implementation fileTree(dir: "libs", include: ["*.jar"])
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ implementation 'androidx.core:core-ktx:1.3.0'
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'com.google.android.material:material:1.1.0'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.1.0'
+ implementation 'androidx.navigation:navigation-ui-ktx:2.1.0'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+
+}
\ No newline at end of file
diff --git a/attachment-viewer/consumer-rules.pro b/attachment-viewer/consumer-rules.pro
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/attachment-viewer/proguard-rules.pro b/attachment-viewer/proguard-rules.pro
new file mode 100644
index 0000000000..481bb43481
--- /dev/null
+++ b/attachment-viewer/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/attachment-viewer/src/main/AndroidManifest.xml b/attachment-viewer/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..4a632774f7
--- /dev/null
+++ b/attachment-viewer/src/main/AndroidManifest.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt
new file mode 100644
index 0000000000..9fd2902970
--- /dev/null
+++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.attachment_viewer
+
+sealed class AttachmentInfo {
+ data class Image(val url: String, val data: Any?) : AttachmentInfo()
+ data class Video(val url: String, val data: Any) : AttachmentInfo()
+ data class Audio(val url: String, val data: Any) : AttachmentInfo()
+ data class File(val url: String, val data: Any) : AttachmentInfo()
+
+ fun bind() {
+ }
+}
+
+interface AttachmentSourceProvider {
+
+ fun getItemCount(): Int
+
+ fun getAttachmentInfoAt(position: Int): AttachmentInfo
+
+ fun loadImage(holder: ImageViewHolder, info: AttachmentInfo.Image)
+}
diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentViewerActivity.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentViewerActivity.kt
new file mode 100644
index 0000000000..2d4cbff00d
--- /dev/null
+++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentViewerActivity.kt
@@ -0,0 +1,210 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.attachment_viewer
+
+import android.graphics.Color
+import android.os.Bundle
+import android.util.Log
+import android.view.MotionEvent
+import android.view.ScaleGestureDetector
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.isVisible
+import androidx.viewpager2.widget.ViewPager2
+import kotlinx.android.synthetic.main.activity_attachment_viewer.*
+import kotlin.math.abs
+
+abstract class AttachmentViewerActivity : AppCompatActivity() {
+
+ lateinit var pager2: ViewPager2
+ lateinit var imageTransitionView: ImageView
+ lateinit var transitionImageContainer: ViewGroup
+
+ // TODO
+ private var overlayView: View? = null
+
+ private lateinit var swipeDismissHandler: SwipeToDismissHandler
+ private lateinit var directionDetector: SwipeDirectionDetector
+ private lateinit var scaleDetector: ScaleGestureDetector
+
+
+ var currentPosition = 0
+
+ private var swipeDirection: SwipeDirection? = null
+
+ private fun isScaled() = attachmentsAdapter.isScaled(currentPosition)
+
+ private var wasScaled: Boolean = false
+ private var isSwipeToDismissAllowed: Boolean = true
+ private lateinit var attachmentsAdapter: AttachmentsAdapter
+
+// private val shouldDismissToBottom: Boolean
+// get() = e == null
+// || !externalTransitionImageView.isRectVisible
+// || !isAtStartPosition
+
+ private var isImagePagerIdle = true
+
+ fun setSourceProvider(sourceProvider: AttachmentSourceProvider) {
+ attachmentsAdapter.attachmentSourceProvider = sourceProvider
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_attachment_viewer)
+ attachmentPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL
+ attachmentsAdapter = AttachmentsAdapter()
+ attachmentPager.adapter = attachmentsAdapter
+ imageTransitionView = transitionImageView
+ transitionImageContainer = findViewById(R.id.transitionImageContainer)
+ pager2 = attachmentPager
+ directionDetector = createSwipeDirectionDetector()
+
+ attachmentPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
+ override fun onPageScrollStateChanged(state: Int) {
+ isImagePagerIdle = state == ViewPager2.SCROLL_STATE_IDLE
+ }
+
+ override fun onPageSelected(position: Int) {
+ currentPosition = position
+ }
+ })
+
+ swipeDismissHandler = createSwipeToDismissHandler()
+ rootContainer.setOnTouchListener(swipeDismissHandler)
+ rootContainer.viewTreeObserver.addOnGlobalLayoutListener { swipeDismissHandler.translationLimit = dismissContainer.height / 4 }
+
+ scaleDetector = createScaleGestureDetector()
+
+ }
+
+ override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
+
+ // The zoomable view is configured to disallow interception when image is zoomed
+
+ // Check if the overlay is visible, and wants to handle the click
+// if (overlayView.isVisible && overlayView?.dispatchTouchEvent(event) == true) {
+// return true
+// }
+
+
+ Log.v("ATTACHEMENTS", "================\ndispatchTouchEvent $ev")
+ handleUpDownEvent(ev)
+
+ Log.v("ATTACHEMENTS", "scaleDetector is in progress ${scaleDetector.isInProgress}")
+ Log.v("ATTACHEMENTS", "pointerCount ${ev.pointerCount}")
+ Log.v("ATTACHEMENTS", "wasScaled ${wasScaled}")
+ if (swipeDirection == null && (scaleDetector.isInProgress || ev.pointerCount > 1 || wasScaled)) {
+ wasScaled = true
+ Log.v("ATTACHEMENTS", "dispatch to pager")
+ return attachmentPager.dispatchTouchEvent(ev)
+ }
+
+
+ Log.v("ATTACHEMENTS", "is current item scaled ${isScaled()}")
+ return (if (isScaled()) super.dispatchTouchEvent(ev) else handleTouchIfNotScaled(ev)).also {
+ Log.v("ATTACHEMENTS", "\n================")
+ }
+ }
+
+ private fun handleUpDownEvent(event: MotionEvent) {
+ Log.v("ATTACHEMENTS", "handleUpDownEvent $event")
+ if (event.action == MotionEvent.ACTION_UP) {
+ handleEventActionUp(event)
+ }
+
+ if (event.action == MotionEvent.ACTION_DOWN) {
+ handleEventActionDown(event)
+ }
+
+ scaleDetector.onTouchEvent(event)
+// gestureDetector.onTouchEvent(event)
+ }
+
+ private fun handleEventActionDown(event: MotionEvent) {
+ swipeDirection = null
+ wasScaled = false
+ attachmentPager.dispatchTouchEvent(event)
+
+ swipeDismissHandler.onTouch(rootContainer, event)
+// isOverlayWasClicked = dispatchOverlayTouch(event)
+ }
+
+ private fun handleEventActionUp(event: MotionEvent) {
+// wasDoubleTapped = false
+ swipeDismissHandler.onTouch(rootContainer, event)
+ attachmentPager.dispatchTouchEvent(event)
+// isOverlayWasClicked = dispatchOverlayTouch(event)
+ }
+
+ private fun handleTouchIfNotScaled(event: MotionEvent): Boolean {
+
+ Log.v("ATTACHEMENTS", "handleTouchIfNotScaled ${event}")
+ directionDetector.handleTouchEvent(event)
+
+ return when (swipeDirection) {
+ SwipeDirection.Up, SwipeDirection.Down -> {
+ if (isSwipeToDismissAllowed && !wasScaled && isImagePagerIdle) {
+ swipeDismissHandler.onTouch(rootContainer, event)
+ } else true
+ }
+ SwipeDirection.Left, SwipeDirection.Right -> {
+ attachmentPager.dispatchTouchEvent(event)
+ }
+ else -> true
+ }
+ }
+
+
+ private fun handleSwipeViewMove(translationY: Float, translationLimit: Int) {
+ val alpha = calculateTranslationAlpha(translationY, translationLimit)
+ backgroundView.alpha = alpha
+ dismissContainer.alpha = alpha
+ overlayView?.alpha = alpha
+ }
+
+ private fun dispatchOverlayTouch(event: MotionEvent): Boolean =
+ overlayView
+ ?.let { it.isVisible && it.dispatchTouchEvent(event) }
+ ?: false
+
+ private fun calculateTranslationAlpha(translationY: Float, translationLimit: Int): Float =
+ 1.0f - 1.0f / translationLimit.toFloat() / 4f * abs(translationY)
+
+ private fun createSwipeToDismissHandler()
+ : SwipeToDismissHandler = SwipeToDismissHandler(
+ swipeView = dismissContainer,
+ shouldAnimateDismiss = { shouldAnimateDismiss() },
+ onDismiss = { animateClose() },
+ onSwipeViewMove = ::handleSwipeViewMove)
+
+ private fun createSwipeDirectionDetector() =
+ SwipeDirectionDetector(this) { swipeDirection = it }
+
+ private fun createScaleGestureDetector() =
+ ScaleGestureDetector(this, ScaleGestureDetector.SimpleOnScaleGestureListener())
+
+
+ protected open fun shouldAnimateDismiss(): Boolean = true
+
+ protected open fun animateClose() {
+ window.statusBarColor = Color.TRANSPARENT
+ finish()
+ }
+}
diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentsAdapter.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentsAdapter.kt
new file mode 100644
index 0000000000..b9914e4dda
--- /dev/null
+++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentsAdapter.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.attachment_viewer
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+
+
+abstract class BaseViewHolder constructor(itemView: View) :
+ RecyclerView.ViewHolder(itemView) {
+
+ abstract fun bind(attachmentInfo: AttachmentInfo)
+}
+
+
+class AttachmentViewHolder constructor(itemView: View) :
+ BaseViewHolder(itemView) {
+
+ override fun bind(attachmentInfo: AttachmentInfo) {
+
+ }
+}
+
+//class AttachmentsAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : FragmentStateAdapter(fragmentManager, lifecycle) {
+class AttachmentsAdapter() : RecyclerView.Adapter() {
+
+ var attachmentSourceProvider: AttachmentSourceProvider? = null
+ set(value) {
+ field = value
+ notifyDataSetChanged()
+ }
+
+ var recyclerView: RecyclerView? = null
+
+ override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
+ this.recyclerView = recyclerView
+ }
+
+ override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
+ this.recyclerView = null
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
+ val inflater = LayoutInflater.from(parent.context)
+ val itemView = inflater.inflate(viewType, parent, false)
+ return when (viewType) {
+ R.layout.item_image_attachment -> ImageViewHolder(itemView)
+ else -> AttachmentViewHolder(itemView)
+ }
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ val info = attachmentSourceProvider!!.getAttachmentInfoAt(position)
+ return when (info) {
+ is AttachmentInfo.Image -> R.layout.item_image_attachment
+ is AttachmentInfo.Video -> R.layout.item_video_attachment
+ is AttachmentInfo.Audio -> TODO()
+ is AttachmentInfo.File -> TODO()
+ }
+
+ }
+
+ override fun getItemCount(): Int {
+ return attachmentSourceProvider?.getItemCount() ?: 0
+ }
+
+ override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
+ attachmentSourceProvider?.getAttachmentInfoAt(position)?.let {
+ holder.bind(it)
+ if (it is AttachmentInfo.Image) {
+ attachmentSourceProvider?.loadImage(holder as ImageViewHolder, it)
+ }
+ }
+ }
+
+ fun isScaled(position: Int): Boolean {
+ val holder = recyclerView?.findViewHolderForAdapterPosition(position)
+ if (holder is ImageViewHolder) {
+ return holder.touchImageView.attacher.scale > 1f
+ }
+ return false
+ }
+
+// override fun getItemCount(): Int {
+// return 8
+// }
+//
+// override fun createFragment(position: Int): Fragment {
+// // Return a NEW fragment instance in createFragment(int)
+// val fragment = DemoObjectFragment()
+// fragment.arguments = Bundle().apply {
+// // Our object is just an integer :-P
+// putInt(ARG_OBJECT, position + 1)
+// }
+// return fragment
+// }
+
+}
+
+
+//private const val ARG_OBJECT = "object"
+//
+//// Instances of this class are fragments representing a single
+//// object in our collection.
+//class DemoObjectFragment : Fragment() {
+//
+// override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+// return inflater.inflate(R.layout.view_image_attachment, container, false)
+// }
+//
+// override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+// arguments?.takeIf { it.containsKey(ARG_OBJECT) }?.apply {
+// val textView: TextView = view.findViewById(R.id.testPage)
+// textView.text = getInt(ARG_OBJECT).toString()
+// }
+// }
+//}
diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ImageViewHolder.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ImageViewHolder.kt
new file mode 100644
index 0000000000..cac6a4fd9e
--- /dev/null
+++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ImageViewHolder.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.attachment_viewer
+
+import android.graphics.drawable.Drawable
+import android.util.Log
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.ProgressBar
+import androidx.core.view.isVisible
+import androidx.core.view.updateLayoutParams
+import com.bumptech.glide.request.target.CustomViewTarget
+import com.bumptech.glide.request.transition.Transition
+import com.github.chrisbanes.photoview.PhotoView
+
+class ImageViewHolder constructor(itemView: View) :
+ BaseViewHolder(itemView) {
+
+ val touchImageView: PhotoView = itemView.findViewById(R.id.touchImageView)
+ val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress)
+
+ init {
+ touchImageView.setAllowParentInterceptOnEdge(false)
+ touchImageView.setOnScaleChangeListener { scaleFactor, _, _ ->
+ Log.v("ATTACHEMENTS", "scaleFactor $scaleFactor")
+ // It's a bit annoying but when you pitch down the scaling
+ // is not exactly one :/
+ touchImageView.setAllowParentInterceptOnEdge(scaleFactor <= 1.0008f)
+ }
+ touchImageView.setScale(1.0f, true)
+ touchImageView.setAllowParentInterceptOnEdge(true)
+ }
+
+ val customTargetView = object : CustomViewTarget(touchImageView) {
+
+ override fun onResourceLoading(placeholder: Drawable?) {
+ imageLoaderProgress.isVisible = true
+ }
+
+ override fun onLoadFailed(errorDrawable: Drawable?) {
+ imageLoaderProgress.isVisible = false
+ }
+
+ override fun onResourceCleared(placeholder: Drawable?) {
+ touchImageView.setImageDrawable(placeholder)
+ }
+
+ override fun onResourceReady(resource: Drawable, transition: Transition?) {
+ imageLoaderProgress.isVisible = false
+ // Glide mess up the view size :/
+ touchImageView.updateLayoutParams {
+ width = LinearLayout.LayoutParams.MATCH_PARENT
+ height = LinearLayout.LayoutParams.MATCH_PARENT
+ }
+ touchImageView.setImageDrawable(resource)
+ }
+ }
+
+ override fun bind(attachmentInfo: AttachmentInfo) {
+ }
+}
diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirection.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirection.kt
new file mode 100644
index 0000000000..fc54d292c2
--- /dev/null
+++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirection.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.attachment_viewer
+
+sealed class SwipeDirection {
+ object NotDetected : SwipeDirection()
+ object Up : SwipeDirection()
+ object Down : SwipeDirection()
+ object Left : SwipeDirection()
+ object Right : SwipeDirection()
+
+ companion object {
+ fun fromAngle(angle: Double): SwipeDirection {
+ return when (angle) {
+ in 0.0..45.0 -> Right
+ in 45.0..135.0 -> Up
+ in 135.0..225.0 -> Left
+ in 225.0..315.0 -> Down
+ in 315.0..360.0 -> Right
+ else -> NotDetected
+ }
+ }
+ }
+}
diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirectionDetector.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirectionDetector.kt
new file mode 100644
index 0000000000..cce37a6d05
--- /dev/null
+++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirectionDetector.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.attachment_viewer
+
+import android.content.Context
+import android.view.MotionEvent
+import kotlin.math.sqrt
+
+class SwipeDirectionDetector(
+ context: Context,
+ private val onDirectionDetected: (SwipeDirection) -> Unit
+) {
+
+ private val touchSlop: Int = android.view.ViewConfiguration.get(context).scaledTouchSlop
+ private var startX: Float = 0f
+ private var startY: Float = 0f
+ private var isDetected: Boolean = false
+
+ fun handleTouchEvent(event: MotionEvent) {
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ startX = event.x
+ startY = event.y
+ }
+ MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
+ if (!isDetected) {
+ onDirectionDetected(SwipeDirection.NotDetected)
+ }
+ startY = 0.0f
+ startX = startY
+ isDetected = false
+ }
+ MotionEvent.ACTION_MOVE -> if (!isDetected && getEventDistance(event) > touchSlop) {
+ isDetected = true
+ onDirectionDetected(getDirection(startX, startY, event.x, event.y))
+ }
+ }
+ }
+
+ /**
+ * Given two points in the plane p1=(x1, x2) and p2=(y1, y1), this method
+ * returns the direction that an arrow pointing from p1 to p2 would have.
+ *
+ * @param x1 the x position of the first point
+ * @param y1 the y position of the first point
+ * @param x2 the x position of the second point
+ * @param y2 the y position of the second point
+ * @return the direction
+ */
+ private fun getDirection(x1: Float, y1: Float, x2: Float, y2: Float): SwipeDirection {
+ val angle = getAngle(x1, y1, x2, y2)
+ return SwipeDirection.fromAngle(angle)
+ }
+
+ /**
+ * Finds the angle between two points in the plane (x1,y1) and (x2, y2)
+ * The angle is measured with 0/360 being the X-axis to the right, angles
+ * increase counter clockwise.
+ *
+ * @param x1 the x position of the first point
+ * @param y1 the y position of the first point
+ * @param x2 the x position of the second point
+ * @param y2 the y position of the second point
+ * @return the angle between two points
+ */
+ private fun getAngle(x1: Float, y1: Float, x2: Float, y2: Float): Double {
+ val rad = Math.atan2((y1 - y2).toDouble(), (x2 - x1).toDouble()) + Math.PI
+ return (rad * 180 / Math.PI + 180) % 360
+ }
+
+ private fun getEventDistance(ev: MotionEvent): Float {
+ val dx = ev.getX(0) - startX
+ val dy = ev.getY(0) - startY
+ return sqrt((dx * dx + dy * dy).toDouble()).toFloat()
+ }
+}
diff --git a/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeToDismissHandler.kt b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeToDismissHandler.kt
new file mode 100644
index 0000000000..3a317d94e2
--- /dev/null
+++ b/attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeToDismissHandler.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.attachment_viewer
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.annotation.SuppressLint
+import android.graphics.Rect
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewPropertyAnimator
+import android.view.animation.AccelerateInterpolator
+
+class SwipeToDismissHandler(
+ private val swipeView: View,
+ private val onDismiss: () -> Unit,
+ private val onSwipeViewMove: (translationY: Float, translationLimit: Int) -> Unit,
+ private val shouldAnimateDismiss: () -> Boolean
+) : View.OnTouchListener {
+
+ companion object {
+ private const val ANIMATION_DURATION = 200L
+ }
+
+ var translationLimit: Int = swipeView.height / 4
+ private var isTracking = false
+ private var startY: Float = 0f
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onTouch(v: View, event: MotionEvent): Boolean {
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ if (swipeView.hitRect.contains(event.x.toInt(), event.y.toInt())) {
+ isTracking = true
+ }
+ startY = event.y
+ return true
+ }
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
+ if (isTracking) {
+ isTracking = false
+ onTrackingEnd(v.height)
+ }
+ return true
+ }
+ MotionEvent.ACTION_MOVE -> {
+ if (isTracking) {
+ val translationY = event.y - startY
+ swipeView.translationY = translationY
+ onSwipeViewMove(translationY, translationLimit)
+ }
+ return true
+ }
+ else -> {
+ return false
+ }
+ }
+ }
+
+ internal fun initiateDismissToBottom() {
+ animateTranslation(swipeView.height.toFloat())
+ }
+
+ private fun onTrackingEnd(parentHeight: Int) {
+ val animateTo = when {
+ swipeView.translationY < -translationLimit -> -parentHeight.toFloat()
+ swipeView.translationY > translationLimit -> parentHeight.toFloat()
+ else -> 0f
+ }
+
+ if (animateTo != 0f && !shouldAnimateDismiss()) {
+ onDismiss()
+ } else {
+ animateTranslation(animateTo)
+ }
+ }
+
+ private fun animateTranslation(translationTo: Float) {
+ swipeView.animate()
+ .translationY(translationTo)
+ .setDuration(ANIMATION_DURATION)
+ .setInterpolator(AccelerateInterpolator())
+ .setUpdateListener { onSwipeViewMove(swipeView.translationY, translationLimit) }
+ .setAnimatorListener(onAnimationEnd = {
+ if (translationTo != 0f) {
+ onDismiss()
+ }
+
+ //remove the update listener, otherwise it will be saved on the next animation execution:
+ swipeView.animate().setUpdateListener(null)
+ })
+ .start()
+ }
+}
+
+internal fun ViewPropertyAnimator.setAnimatorListener(
+ onAnimationEnd: ((Animator?) -> Unit)? = null,
+ onAnimationStart: ((Animator?) -> Unit)? = null
+) = this.setListener(
+ object : AnimatorListenerAdapter() {
+
+ override fun onAnimationEnd(animation: Animator?) {
+ onAnimationEnd?.invoke(animation)
+ }
+
+ override fun onAnimationStart(animation: Animator?) {
+ onAnimationStart?.invoke(animation)
+ }
+ })
+
+internal val View?.hitRect: Rect
+ get() = Rect().also { this?.getHitRect(it) }
diff --git a/attachment-viewer/src/main/res/layout/activity_attachment_viewer.xml b/attachment-viewer/src/main/res/layout/activity_attachment_viewer.xml
new file mode 100644
index 0000000000..a8a68db1a5
--- /dev/null
+++ b/attachment-viewer/src/main/res/layout/activity_attachment_viewer.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/attachment-viewer/src/main/res/layout/item_image_attachment.xml b/attachment-viewer/src/main/res/layout/item_image_attachment.xml
new file mode 100644
index 0000000000..91a009df2a
--- /dev/null
+++ b/attachment-viewer/src/main/res/layout/item_image_attachment.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/attachment-viewer/src/main/res/layout/item_video_attachment.xml b/attachment-viewer/src/main/res/layout/item_video_attachment.xml
new file mode 100644
index 0000000000..9449ec2e9f
--- /dev/null
+++ b/attachment-viewer/src/main/res/layout/item_video_attachment.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/attachment-viewer/src/main/res/layout/view_image_attachment.xml b/attachment-viewer/src/main/res/layout/view_image_attachment.xml
new file mode 100644
index 0000000000..3518a4472d
--- /dev/null
+++ b/attachment-viewer/src/main/res/layout/view_image_attachment.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/attachment-viewer/src/main/res/values/dimens.xml b/attachment-viewer/src/main/res/values/dimens.xml
new file mode 100644
index 0000000000..125df87119
--- /dev/null
+++ b/attachment-viewer/src/main/res/values/dimens.xml
@@ -0,0 +1,3 @@
+
+ 16dp
+
\ No newline at end of file
diff --git a/attachment-viewer/src/main/res/values/strings.xml b/attachment-viewer/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..6dcb56555a
--- /dev/null
+++ b/attachment-viewer/src/main/res/values/strings.xml
@@ -0,0 +1,11 @@
+
+ AttachementViewerActivity
+
+ First Fragment
+ Second Fragment
+ Next
+ Previous
+
+ Hello first fragment
+ Hello second fragment. Arg: %1$s
+
\ No newline at end of file
diff --git a/attachment-viewer/src/main/res/values/styles.xml b/attachment-viewer/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..a81174782e
--- /dev/null
+++ b/attachment-viewer/src/main/res/values/styles.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index af3952b2d3..47b3ab240d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -39,6 +39,8 @@ allprojects {
includeGroupByRegex "com\\.github\\.yalantis"
// JsonViewer
includeGroupByRegex 'com\\.github\\.BillCarsonFr'
+ // PhotoView
+ includeGroupByRegex 'com\\.github\\.chrisbanes'
}
}
maven {
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt
index a69127532e..bdbbbf11bd 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt
@@ -39,4 +39,6 @@ interface TimelineService {
fun getTimeLineEvent(eventId: String): TimelineEvent?
fun getTimeLineEventLive(eventId: String): LiveData>
+
+ fun getAttachementMessages() : List
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt
index 5723568197..ebdb8dd24d 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt
@@ -21,19 +21,24 @@ import androidx.lifecycle.Transformations
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy
+import im.vector.matrix.android.api.session.events.model.isImageMessage
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional
+import im.vector.matrix.android.internal.crypto.store.db.doWithRealm
import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
+import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.fetchCopyMap
+import io.realm.Sort
+import io.realm.kotlin.where
import org.greenrobot.eventbus.EventBus
internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String,
@@ -73,10 +78,10 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
override fun getTimeLineEvent(eventId: String): TimelineEvent? {
return monarchy
.fetchCopyMap({
- TimelineEventEntity.where(it, roomId = roomId, eventId = eventId).findFirst()
- }, { entity, _ ->
- timelineEventMapper.map(entity)
- })
+ TimelineEventEntity.where(it, roomId = roomId, eventId = eventId).findFirst()
+ }, { entity, _ ->
+ timelineEventMapper.map(entity)
+ })
}
override fun getTimeLineEventLive(eventId: String): LiveData> {
@@ -88,4 +93,16 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
events.firstOrNull().toOptional()
}
}
+
+ override fun getAttachementMessages(): List {
+ // TODO pretty bad query.. maybe we should denormalize clear type in base?
+ return doWithRealm(monarchy.realmConfiguration) { realm ->
+ realm.where()
+ .equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
+ .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
+ .findAll()
+ ?.mapNotNull { timelineEventMapper.map(it).takeIf { it.root.isImageMessage() } }
+ ?: emptyList()
+ }
+ }
}
diff --git a/settings.gradle b/settings.gradle
index 04307e89d9..3a7aa9ac1c 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,2 +1,2 @@
-include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch'
-include ':multipicker'
+include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch', ':attachment-viewer'
+include ':multipicker'
\ No newline at end of file
diff --git a/vector/build.gradle b/vector/build.gradle
index 59ae3d35de..b409a7d8b8 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -279,6 +279,7 @@ dependencies {
implementation project(":matrix-sdk-android-rx")
implementation project(":diff-match-patch")
implementation project(":multipicker")
+ implementation project(":attachment-viewer")
implementation 'com.android.support:multidex:1.0.3'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
@@ -368,6 +369,10 @@ dependencies {
implementation "com.github.piasy:GlideImageLoader:$big_image_viewer_version"
implementation "com.github.piasy:ProgressPieIndicator:$big_image_viewer_version"
implementation "com.github.piasy:GlideImageViewFactory:$big_image_viewer_version"
+
+ // implementation 'com.github.MikeOrtiz:TouchImageView:3.0.2'
+ implementation 'com.github.chrisbanes:PhotoView:2.0.0'
+
implementation "com.github.bumptech.glide:glide:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version"
implementation 'com.danikula:videocache:2.7.1'
diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index f9b78db17c..155c3bcd64 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -85,6 +85,11 @@
+
+
+
+ navigator.openImageViewer(requireActivity(), roomDetailArgs.roomId, mediaData, view) { pairs ->
pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
}
diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt
index eeeb55ed15..7cd7ba56e5 100644
--- a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt
+++ b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt
@@ -19,11 +19,13 @@ package im.vector.riotx.features.media
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Parcelable
+import android.view.View
import android.widget.ImageView
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestListener
+import com.bumptech.glide.request.target.CustomViewTarget
import com.bumptech.glide.request.target.Target
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.ORIENTATION_USE_EXIF
import com.github.piasy.biv.view.BigImageView
@@ -93,6 +95,25 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
.into(imageView)
}
+ fun render(data: Data, contextView: View, target: CustomViewTarget<*, Drawable>) {
+ val req = if (data.elementToDecrypt != null) {
+ // Encrypted image
+ GlideApp
+ .with(contextView)
+ .load(data)
+ } else {
+ // Clear image
+ val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver().resolveFullSize(data.url)
+ GlideApp
+ .with(contextView)
+ .load(resolvedUrl)
+ }
+
+ req.override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
+ .fitCenter()
+ .into(target)
+ }
+
fun renderFitTarget(data: Data, mode: Mode, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) {
val size = processSize(data, mode)
@@ -122,6 +143,48 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
.into(imageView)
}
+ fun renderThumbnailDontTransform(data: Data, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) {
+
+ // a11y
+ imageView.contentDescription = data.filename
+
+ val req = if (data.elementToDecrypt != null) {
+ // Encrypted image
+ GlideApp
+ .with(imageView)
+ .load(data)
+ } else {
+ // Clear image
+ val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver().resolveFullSize(data.url)
+ GlideApp
+ .with(imageView)
+ .load(resolvedUrl)
+ }
+
+ req.listener(object : RequestListener {
+ override fun onLoadFailed(e: GlideException?,
+ model: Any?,
+ target: Target?,
+ isFirstResource: Boolean): Boolean {
+ callback?.invoke(false)
+ return false
+ }
+
+ override fun onResourceReady(resource: Drawable?,
+ model: Any?,
+ target: Target?,
+ dataSource: DataSource?,
+ isFirstResource: Boolean): Boolean {
+ callback?.invoke(true)
+ return false
+ }
+ })
+ .dontTransform()
+ .into(imageView)
+
+
+ }
+
private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest {
return if (data.elementToDecrypt != null) {
// Encrypted image
diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt
index 092199759f..8a6c2f7545 100644
--- a/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/media/ImageMediaViewerActivity.kt
@@ -91,6 +91,8 @@ class ImageMediaViewerActivity : VectorBaseActivity() {
encryptedImageView.isVisible = false
// Postpone transaction a bit until thumbnail is loaded
supportPostponeEnterTransition()
+
+ // We are not passing the exact same image that in the
imageContentRenderer.renderFitTarget(mediaData, ImageContentRenderer.Mode.THUMBNAIL, imageTransitionView) {
// Proceed with transaction
scheduleStartPostponedTransition(imageTransitionView)
diff --git a/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt
new file mode 100644
index 0000000000..991ecaafde
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.media
+
+import android.graphics.drawable.Drawable
+import com.bumptech.glide.request.target.CustomViewTarget
+import im.vector.matrix.android.api.session.events.model.toModel
+import im.vector.matrix.android.api.session.room.model.message.MessageContent
+import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent
+import im.vector.matrix.android.api.session.room.model.message.getFileUrl
+import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
+import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
+import im.vector.riotx.attachment_viewer.AttachmentInfo
+import im.vector.riotx.attachment_viewer.AttachmentSourceProvider
+import im.vector.riotx.attachment_viewer.ImageViewHolder
+import javax.inject.Inject
+
+class RoomAttachmentProvider(
+ private val attachments: List,
+ private val initialIndex: Int,
+ private val imageContentRenderer: ImageContentRenderer
+) : AttachmentSourceProvider {
+
+ override fun getItemCount(): Int {
+ return attachments.size
+ }
+
+ override fun getAttachmentInfoAt(position: Int): AttachmentInfo {
+ return attachments[position].let {
+ val content = it.root.getClearContent().toModel() as? MessageWithAttachmentContent
+ val data = ImageContentRenderer.Data(
+ eventId = it.eventId,
+ filename = content?.body ?: "",
+ mimeType = content?.mimeType,
+ url = content?.getFileUrl(),
+ elementToDecrypt = content?.encryptedFileInfo?.toElementToDecrypt(),
+ maxHeight = -1,
+ maxWidth = -1,
+ width = null,
+ height = null
+ )
+ AttachmentInfo.Image(
+ content?.url ?: "",
+ data
+ )
+ }
+ }
+
+ override fun loadImage(holder: ImageViewHolder, info: AttachmentInfo.Image) {
+ (info.data as? ImageContentRenderer.Data)?.let {
+ imageContentRenderer.render(it, holder.touchImageView, holder.customTargetView as CustomViewTarget<*, Drawable>)
+ }
+ }
+// override fun loadImage(holder: ImageViewHolder, info: AttachmentInfo.Image) {
+// (info.data as? ImageContentRenderer.Data)?.let {
+// imageContentRenderer.render(it, ImageContentRenderer.Mode.FULL_SIZE, holder.touchImageView)
+// }
+// }
+}
+
+class RoomAttachmentProviderFactory @Inject constructor(
+ private val imageContentRenderer: ImageContentRenderer
+) {
+
+ fun createProvider(attachments: List, initialIndex: Int): RoomAttachmentProvider {
+ return RoomAttachmentProvider(attachments, initialIndex, imageContentRenderer)
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt
new file mode 100644
index 0000000000..2df8bfd0f6
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt
@@ -0,0 +1,207 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package im.vector.riotx.features.media
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.Parcelable
+import android.view.View
+import android.view.ViewTreeObserver
+import androidx.core.app.ActivityCompat
+import androidx.core.transition.addListener
+import androidx.core.view.ViewCompat
+import androidx.core.view.isInvisible
+import androidx.core.view.isVisible
+import androidx.transition.Transition
+import im.vector.riotx.attachment_viewer.AttachmentViewerActivity
+import im.vector.riotx.core.di.ActiveSessionHolder
+import im.vector.riotx.core.di.DaggerScreenComponent
+import im.vector.riotx.core.di.HasVectorInjector
+import im.vector.riotx.core.di.ScreenComponent
+import im.vector.riotx.core.di.VectorComponent
+import im.vector.riotx.features.themes.ActivityOtherThemes
+import im.vector.riotx.features.themes.ThemeUtils
+import kotlinx.android.parcel.Parcelize
+import timber.log.Timber
+import javax.inject.Inject
+import kotlin.system.measureTimeMillis
+
+class VectorAttachmentViewerActivity : AttachmentViewerActivity() {
+
+ @Parcelize
+ data class Args(
+ val roomId: String?,
+ val eventId: String,
+ val sharedTransitionName: String?
+ ) : Parcelable
+
+ @Inject
+ lateinit var sessionHolder: ActiveSessionHolder
+
+ @Inject
+ lateinit var dataSourceFactory: RoomAttachmentProviderFactory
+
+ @Inject
+ lateinit var imageContentRenderer: ImageContentRenderer
+
+ private lateinit var screenComponent: ScreenComponent
+
+ private var initialIndex = 0
+ private var isAnimatingOut = false
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+
+ super.onCreate(savedInstanceState)
+ Timber.i("onCreate Activity ${this.javaClass.simpleName}")
+ val vectorComponent = getVectorComponent()
+ screenComponent = DaggerScreenComponent.factory().create(vectorComponent, this)
+ val timeForInjection = measureTimeMillis {
+ screenComponent.inject(this)
+ }
+ Timber.v("Injecting dependencies into ${javaClass.simpleName} took $timeForInjection ms")
+ ThemeUtils.setActivityTheme(this, getOtherThemes())
+
+ val args = args() ?: throw IllegalArgumentException("Missing arguments")
+ val session = sessionHolder.getSafeActiveSession() ?: return Unit.also { finish() }
+
+ val room = args.roomId?.let { session.getRoom(it) }
+ val events = room?.getAttachementMessages() ?: emptyList()
+ val index = events.indexOfFirst { it.eventId == args.eventId }
+ initialIndex = index
+
+
+ if (savedInstanceState == null && addTransitionListener()) {
+ args.sharedTransitionName?.let {
+ ViewCompat.setTransitionName(imageTransitionView, it)
+ transitionImageContainer.isVisible = true
+
+ // Postpone transaction a bit until thumbnail is loaded
+ val mediaData: ImageContentRenderer.Data? = intent.getParcelableExtra(EXTRA_IMAGE_DATA)
+ if (mediaData != null) {
+ // will be shown at end of transition
+ pager2.isInvisible = true
+ supportPostponeEnterTransition()
+ imageContentRenderer.renderThumbnailDontTransform(mediaData, imageTransitionView) {
+ // Proceed with transaction
+ scheduleStartPostponedTransition(imageTransitionView)
+ }
+ }
+ }
+ }
+
+ setSourceProvider(dataSourceFactory.createProvider(events, index))
+ if (savedInstanceState == null) {
+ pager2.setCurrentItem(index, false)
+ }
+
+ }
+
+ private fun getOtherThemes() = ActivityOtherThemes.VectorAttachmentsPreview
+
+
+ override fun shouldAnimateDismiss(): Boolean {
+ return currentPosition != initialIndex
+ }
+
+ override fun onBackPressed() {
+ if (currentPosition == initialIndex) {
+ // show back the transition view
+ // TODO, we should track and update the mapping
+ transitionImageContainer.isVisible = true
+ }
+ isAnimatingOut = true
+ super.onBackPressed()
+ }
+
+ override fun animateClose() {
+ if (currentPosition == initialIndex) {
+ // show back the transition view
+ // TODO, we should track and update the mapping
+ transitionImageContainer.isVisible = true
+ }
+ isAnimatingOut = true
+ ActivityCompat.finishAfterTransition(this);
+ }
+
+ /* ==========================================================================================
+ * PRIVATE METHODS
+ * ========================================================================================== */
+
+ /**
+ * Try and add a [Transition.TransitionListener] to the entering shared element
+ * [Transition]. We do this so that we can load the full-size image after the transition
+ * has completed.
+ *
+ * @return true if we were successful in adding a listener to the enter transition
+ */
+ private fun addTransitionListener(): Boolean {
+ val transition = window.sharedElementEnterTransition
+
+ if (transition != null) {
+ // There is an entering shared element transition so add a listener to it
+ transition.addListener(
+ onEnd = {
+ if (!isAnimatingOut) {
+ // The listener is also called when we are exiting
+ transitionImageContainer.isVisible = false
+ pager2.isInvisible = false
+ }
+ },
+ onCancel = {
+ if (!isAnimatingOut) {
+ transitionImageContainer.isVisible = false
+ pager2.isInvisible = false
+ }
+ }
+ )
+ return true
+ }
+
+ // If we reach here then we have not added a listener
+ return false
+ }
+
+ private fun args() = intent.getParcelableExtra(EXTRA_ARGS)
+
+
+ private fun getVectorComponent(): VectorComponent {
+ return (application as HasVectorInjector).injector()
+ }
+
+ private fun scheduleStartPostponedTransition(sharedElement: View) {
+ sharedElement.viewTreeObserver.addOnPreDrawListener(
+ object : ViewTreeObserver.OnPreDrawListener {
+ override fun onPreDraw(): Boolean {
+ sharedElement.viewTreeObserver.removeOnPreDrawListener(this)
+ supportStartPostponedEnterTransition()
+ return true
+ }
+ })
+ }
+
+ companion object {
+
+ const val EXTRA_ARGS = "EXTRA_ARGS"
+ const val EXTRA_IMAGE_DATA = "EXTRA_IMAGE_DATA"
+
+ fun newIntent(context: Context, mediaData: ImageContentRenderer.Data, roomId: String?, eventId: String, sharedTransitionName: String?) = Intent(context, VectorAttachmentViewerActivity::class.java).also {
+ it.putExtra(EXTRA_ARGS, Args(roomId, eventId, sharedTransitionName))
+ it.putExtra(EXTRA_IMAGE_DATA, mediaData)
+ }
+
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
index 0b89ab8ec4..debd58e6d2 100644
--- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
+++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt
@@ -49,11 +49,7 @@ import im.vector.riotx.features.home.room.detail.RoomDetailArgs
import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
import im.vector.riotx.features.invite.InviteUsersToRoomActivity
-import im.vector.riotx.features.media.BigImageViewerActivity
-import im.vector.riotx.features.media.ImageContentRenderer
-import im.vector.riotx.features.media.ImageMediaViewerActivity
-import im.vector.riotx.features.media.VideoContentRenderer
-import im.vector.riotx.features.media.VideoMediaViewerActivity
+import im.vector.riotx.features.media.*
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity
@@ -89,7 +85,8 @@ class DefaultNavigator @Inject constructor(
override fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) {
val session = sessionHolder.getSafeActiveSession() ?: return
- val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId) ?: return
+ val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId)
+ ?: return
(tx as? IncomingSasVerificationTransaction)?.performAccept()
if (context is VectorBaseActivity) {
VerificationBottomSheet.withArgs(
@@ -216,7 +213,8 @@ class DefaultNavigator @Inject constructor(
?.let { avatarUrl ->
val intent = BigImageViewerActivity.newIntent(activity, matrixItem.getBestName(), avatarUrl)
val options = sharedElement?.let {
- ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it) ?: "")
+ ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it)
+ ?: "")
}
activity.startActivity(intent, options?.toBundle())
}
@@ -244,22 +242,38 @@ class DefaultNavigator @Inject constructor(
context.startActivity(WidgetActivity.newIntent(context, widgetArgs))
}
- override fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?) {
- val intent = ImageMediaViewerActivity.newIntent(activity, mediaData, ViewCompat.getTransitionName(view))
- val pairs = ArrayList>()
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let {
- pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME))
+ override fun openImageViewer(activity: Activity, roomId: String?, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?) {
+ VectorAttachmentViewerActivity.newIntent(activity, mediaData, roomId, mediaData.eventId, ViewCompat.getTransitionName(view)).let { intent ->
+ val pairs = ArrayList>()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let {
+ pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME))
+ }
+ activity.window.decorView.findViewById(android.R.id.navigationBarBackground)?.let {
+ pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME))
+ }
}
- activity.window.decorView.findViewById(android.R.id.navigationBarBackground)?.let {
- pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME))
- }
- }
- pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: ""))
- options?.invoke(pairs)
+ pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: ""))
+ options?.invoke(pairs)
- val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle()
- activity.startActivity(intent, bundle)
+ val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle()
+ activity.startActivity(intent, bundle)
+ }
+// val intent = ImageMediaViewerActivity.newIntent(activity, mediaData, ViewCompat.getTransitionName(view))
+// val pairs = ArrayList>()
+// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+// activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let {
+// pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME))
+// }
+// activity.window.decorView.findViewById(android.R.id.navigationBarBackground)?.let {
+// pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME))
+// }
+// }
+// pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: ""))
+// options?.invoke(pairs)
+//
+// val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle()
+// activity.startActivity(intent, bundle)
}
override fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data) {
diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
index ce4d5ef3ea..54c0f55a7b 100644
--- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
+++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt
@@ -91,7 +91,7 @@ interface Navigator {
fun openRoomWidget(context: Context, roomId: String, widget: Widget)
- fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?)
+ fun openImageViewer(activity: Activity, roomId: String?, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?)
fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data)
}
diff --git a/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt
index 78a0cece41..e5b2f34f61 100644
--- a/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt
+++ b/vector/src/main/java/im/vector/riotx/features/popup/PopupAlertManager.kt
@@ -26,6 +26,7 @@ import com.tapadoo.alerter.Alerter
import com.tapadoo.alerter.OnHideAlertListener
import dagger.Lazy
import im.vector.riotx.R
+import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.themes.ThemeUtils
import timber.log.Timber
@@ -83,7 +84,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy
+
+
\ No newline at end of file