From 4a2a6d34aebbb2383a71eaa0ad99c24dac81cabd Mon Sep 17 00:00:00 2001 From: Valere Date: Sun, 5 Jul 2020 21:47:38 +0200 Subject: [PATCH] Initial commit --- attachment-viewer/.gitignore | 1 + attachment-viewer/build.gradle | 81 +++++++ attachment-viewer/consumer-rules.pro | 0 attachment-viewer/proguard-rules.pro | 21 ++ .../src/main/AndroidManifest.xml | 11 + .../AttachmentSourceProvider.kt | 36 +++ .../AttachmentViewerActivity.kt | 210 ++++++++++++++++++ .../attachment_viewer/AttachmentsAdapter.kt | 133 +++++++++++ .../attachment_viewer/ImageViewHolder.kt | 75 +++++++ .../riotx/attachment_viewer/SwipeDirection.kt | 38 ++++ .../SwipeDirectionDetector.kt | 90 ++++++++ .../SwipeToDismissHandler.kt | 126 +++++++++++ .../res/layout/activity_attachment_viewer.xml | 46 ++++ .../main/res/layout/item_image_attachment.xml | 22 ++ .../main/res/layout/item_video_attachment.xml | 26 +++ .../main/res/layout/view_image_attachment.xml | 17 ++ .../src/main/res/values/dimens.xml | 3 + .../src/main/res/values/strings.xml | 11 + .../src/main/res/values/styles.xml | 12 + build.gradle | 2 + .../session/room/timeline/TimelineService.kt | 2 + .../room/timeline/DefaultTimelineService.kt | 25 ++- settings.gradle | 4 +- vector/build.gradle | 5 + vector/src/main/AndroidManifest.xml | 5 + .../vector/riotx/core/di/ScreenComponent.kt | 2 + .../home/room/detail/RoomDetailFragment.kt | 2 +- .../features/media/ImageContentRenderer.kt | 63 ++++++ .../media/ImageMediaViewerActivity.kt | 2 + .../features/media/RoomAttachmentProvider.kt | 82 +++++++ .../media/VectorAttachmentViewerActivity.kt | 207 +++++++++++++++++ .../features/navigation/DefaultNavigator.kt | 56 +++-- .../riotx/features/navigation/Navigator.kt | 2 +- .../riotx/features/popup/PopupAlertManager.kt | 3 +- .../uploads/media/RoomUploadsMediaFragment.kt | 2 +- .../features/themes/ActivityOtherThemes.kt | 6 + vector/src/main/res/values/theme_common.xml | 11 + 37 files changed, 1409 insertions(+), 31 deletions(-) create mode 100644 attachment-viewer/.gitignore create mode 100644 attachment-viewer/build.gradle create mode 100644 attachment-viewer/consumer-rules.pro create mode 100644 attachment-viewer/proguard-rules.pro create mode 100644 attachment-viewer/src/main/AndroidManifest.xml create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentSourceProvider.kt create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentViewerActivity.kt create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/AttachmentsAdapter.kt create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/ImageViewHolder.kt create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirection.kt create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeDirectionDetector.kt create mode 100644 attachment-viewer/src/main/java/im/vector/riotx/attachment_viewer/SwipeToDismissHandler.kt create mode 100644 attachment-viewer/src/main/res/layout/activity_attachment_viewer.xml create mode 100644 attachment-viewer/src/main/res/layout/item_image_attachment.xml create mode 100644 attachment-viewer/src/main/res/layout/item_video_attachment.xml create mode 100644 attachment-viewer/src/main/res/layout/view_image_attachment.xml create mode 100644 attachment-viewer/src/main/res/values/dimens.xml create mode 100644 attachment-viewer/src/main/res/values/strings.xml create mode 100644 attachment-viewer/src/main/res/values/styles.xml create mode 100644 vector/src/main/java/im/vector/riotx/features/media/RoomAttachmentProvider.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/media/VectorAttachmentViewerActivity.kt 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