diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index f81ef9d3..87b7263d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -208,6 +208,7 @@ class MessageItemFactory @Inject constructor( val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val data = ImageContentRenderer.Data( + eventId = informationData.eventId, filename = messageContent.body, url = messageContent.getFileUrl(), elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), @@ -251,6 +252,7 @@ class MessageItemFactory @Inject constructor( val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val thumbnailData = ImageContentRenderer.Data( + eventId = informationData.eventId, filename = messageContent.body, url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl, 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 7a7c880c..50970321 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 @@ -42,6 +42,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: @Parcelize data class Data( + val eventId: String, val filename: String, val url: String?, val elementToDecrypt: ElementToDecrypt?, 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 a44672a5..8564f20e 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 @@ -21,6 +21,8 @@ import android.content.Intent import android.graphics.drawable.Drawable import android.os.Build import android.os.Bundle +import android.view.Menu +import android.view.MenuItem import android.view.View import android.view.ViewTreeObserver import androidx.annotation.RequiresApi @@ -36,6 +38,7 @@ import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.Target import com.github.piasy.biv.indicator.progresspie.ProgressPieIndicator import com.github.piasy.biv.view.GlideImageViewFactory +import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.platform.VectorBaseActivity @@ -47,17 +50,20 @@ import javax.inject.Inject class ImageMediaViewerActivity : VectorBaseActivity() { @Inject lateinit var imageContentRenderer: ImageContentRenderer + @Inject lateinit var mediaDownloadHelper: MediaDownloadHelper lateinit var mediaData: ImageContentRenderer.Data + override fun getMenuRes() = R.menu.image_media_viewer + override fun injectWith(injector: ScreenComponent) { injector.inject(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(im.vector.riotx.R.layout.activity_image_media_viewer) - mediaData = intent.getParcelableExtra(EXTRA_MEDIA_DATA) + setContentView(R.layout.activity_image_media_viewer) + mediaData = intent.getParcelableExtra(EXTRA_MEDIA_DATA) intent.extras.getString(EXTRA_SHARED_TRANSITION_NAME)?.let { ViewCompat.setTransitionName(imageTransitionView, it) } @@ -105,6 +111,29 @@ class ImageMediaViewerActivity : VectorBaseActivity() { } } + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + val downloadItem = menu.findItem(R.id.download_image) + downloadItem.isVisible = !mediaData.isLocalFile() + return super.onPrepareOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.download_image -> mediaDownloadHelper.checkPermissionAndDownload( + mediaData.eventId, + mediaData.filename, + mediaData.url, + mediaData.elementToDecrypt + ) + } + return super.onOptionsItemSelected(item) + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + mediaDownloadHelper.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + + private fun configureToolbar(toolbar: Toolbar, mediaData: ImageContentRenderer.Data) { setSupportActionBar(toolbar) supportActionBar?.apply { diff --git a/vector/src/main/java/im/vector/riotx/features/media/MediaDownloadHelper.kt b/vector/src/main/java/im/vector/riotx/features/media/MediaDownloadHelper.kt new file mode 100644 index 00000000..92df3955 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/media/MediaDownloadHelper.kt @@ -0,0 +1,84 @@ +/* + + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + + */ +package im.vector.riotx.features.media + +import androidx.appcompat.app.AppCompatActivity +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.file.FileService +import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt +import im.vector.riotx.R +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.utils.* +import java.io.File +import javax.inject.Inject + +class MediaDownloadHelper @Inject constructor(private val activity: AppCompatActivity, + private val session: Session, + private val stringProvider: StringProvider, + private val errorFormatter: ErrorFormatter) { + + private data class PendingData( + val id: String, + val filename: String, + val url: String, + val elementToDecrypt: ElementToDecrypt? + ) + + private var pendingData: PendingData? = null + + fun checkPermissionAndDownload(id: String, filename: String, url: String?, elementToDecrypt: ElementToDecrypt?) { + if (url.isNullOrEmpty()) { + activity.toast(stringProvider.getString(R.string.unexpected_error)) + } else if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, activity, PERMISSION_REQUEST_CODE_DOWNLOAD_FILE)) { + downloadFile(id, filename, url, elementToDecrypt) + } else { + pendingData = PendingData(id, filename, url, elementToDecrypt) + } + } + + fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + if (allGranted(grantResults) && requestCode == PERMISSION_REQUEST_CODE_DOWNLOAD_FILE) { + pendingData?.also { + downloadFile(it.id, it.filename, it.url, it.elementToDecrypt) + } + } + } + + private fun downloadFile(id: String, filename: String, url: String, elementToDecrypt: ElementToDecrypt?) { + session.downloadFile( + FileService.DownloadMode.TO_EXPORT, + id, + filename, + url, + elementToDecrypt, + object : MatrixCallback { + override fun onSuccess(data: File) { + activity.toast(stringProvider.getString(R.string.downloaded_file, data.path)) + } + + override fun onFailure(failure: Throwable) { + activity.toast(errorFormatter.toHumanReadable(failure)) + } + }) + + } + + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt index 22650b0e..07c60b50 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VideoContentRenderer.kt @@ -43,7 +43,13 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder: val url: String?, val elementToDecrypt: ElementToDecrypt?, val thumbnailMediaData: ImageContentRenderer.Data - ) : Parcelable + ) : Parcelable { + + fun isLocalFile(): Boolean { + return url != null && File(url).exists() + } + + } fun render(data: Data, thumbnailView: ImageView, diff --git a/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt index 093e257c..7b423f9c 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt @@ -19,7 +19,10 @@ package im.vector.riotx.features.media import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.Menu +import android.view.MenuItem import androidx.appcompat.widget.Toolbar +import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.platform.VectorBaseActivity import kotlinx.android.synthetic.main.activity_video_media_viewer.* @@ -30,6 +33,10 @@ class VideoMediaViewerActivity : VectorBaseActivity() { @Inject lateinit var imageContentRenderer: ImageContentRenderer @Inject lateinit var videoContentRenderer: VideoContentRenderer + @Inject lateinit var mediaDownloadHelper: MediaDownloadHelper + lateinit var mediaData: VideoContentRenderer.Data + + override fun getMenuRes() = R.menu.video_media_viewer override fun injectWith(injector: ScreenComponent) { injector.inject(this) @@ -37,9 +44,12 @@ class VideoMediaViewerActivity : VectorBaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(im.vector.riotx.R.layout.activity_video_media_viewer) - val mediaData = intent.getParcelableExtra(EXTRA_MEDIA_DATA) - + setContentView(R.layout.activity_video_media_viewer) + mediaData = intent.getParcelableExtra(EXTRA_MEDIA_DATA) + if (mediaData.url.isNullOrEmpty()) { + finish() + return + } configureToolbar(videoMediaViewerToolbar, mediaData) imageContentRenderer.render(mediaData.thumbnailMediaData, ImageContentRenderer.Mode.FULL_SIZE, videoMediaViewerThumbnailView) videoContentRenderer.render(mediaData, videoMediaViewerThumbnailView, videoMediaViewerLoading, videoMediaViewerVideoView, videoMediaViewerErrorView) @@ -54,6 +64,28 @@ class VideoMediaViewerActivity : VectorBaseActivity() { } } + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + val downloadItem = menu.findItem(R.id.download_video) + downloadItem.isVisible = !mediaData.isLocalFile() + return super.onPrepareOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.download_video -> mediaDownloadHelper.checkPermissionAndDownload( + mediaData.eventId, + mediaData.filename, + mediaData.url, + mediaData.elementToDecrypt + ) + } + return super.onOptionsItemSelected(item) + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + mediaDownloadHelper.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + companion object { private const val EXTRA_MEDIA_DATA = "EXTRA_MEDIA_DATA" diff --git a/vector/src/main/res/menu/image_media_viewer.xml b/vector/src/main/res/menu/image_media_viewer.xml new file mode 100644 index 00000000..290d9385 --- /dev/null +++ b/vector/src/main/res/menu/image_media_viewer.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/menu/video_media_viewer.xml b/vector/src/main/res/menu/video_media_viewer.xml new file mode 100644 index 00000000..ac71a2e2 --- /dev/null +++ b/vector/src/main/res/menu/video_media_viewer.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file