Merge pull request #1242 from vector-im/feature/save_media_to_gallery

Save media files to Gallery
This commit is contained in:
Onuray Sahin 2020-04-20 13:24:33 +03:00 committed by GitHub
commit 5795b7e063
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 206 additions and 3 deletions

View File

@ -6,6 +6,7 @@ Features ✨:
- Cross-Signing | Support SSSS secret sharing (#944)
- Cross-Signing | Verify new session from existing session (#1134)
- Cross-Signing | Bootstraping cross signing with 4S from mobile (#985)
- Save media files to Gallery (#973)
Improvements 🙌:
- Verification DM / Handle concurrent .start after .ready (#794)

View File

@ -29,6 +29,8 @@ import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment
import im.vector.riotx.BuildConfig
import im.vector.riotx.R
import okio.buffer
import okio.sink
import timber.log.Timber
import java.io.File
import java.text.SimpleDateFormat
@ -258,6 +260,61 @@ fun shareMedia(context: Context, file: File, mediaMimeType: String?) {
}
}
fun saveMedia(context: Context, file: File, title: String, mediaMimeType: String?): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val externalContentUri: Uri
val values = ContentValues()
when {
mediaMimeType?.startsWith("image/") == true -> {
externalContentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
values.put(MediaStore.Images.Media.TITLE, title)
values.put(MediaStore.Images.Media.DISPLAY_NAME, title)
values.put(MediaStore.Images.Media.MIME_TYPE, mediaMimeType)
values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis())
values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
}
mediaMimeType?.startsWith("video/") == true -> {
externalContentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
values.put(MediaStore.Video.Media.TITLE, title)
values.put(MediaStore.Video.Media.DISPLAY_NAME, title)
values.put(MediaStore.Video.Media.MIME_TYPE, mediaMimeType)
values.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis())
values.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis())
}
mediaMimeType?.startsWith("audio/") == true -> {
externalContentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
values.put(MediaStore.Audio.Media.TITLE, title)
values.put(MediaStore.Audio.Media.DISPLAY_NAME, title)
values.put(MediaStore.Audio.Media.MIME_TYPE, mediaMimeType)
values.put(MediaStore.Audio.Media.DATE_ADDED, System.currentTimeMillis())
values.put(MediaStore.Audio.Media.DATE_TAKEN, System.currentTimeMillis())
}
else -> {
externalContentUri = MediaStore.Downloads.EXTERNAL_CONTENT_URI
values.put(MediaStore.Downloads.TITLE, title)
values.put(MediaStore.Downloads.DISPLAY_NAME, title)
values.put(MediaStore.Downloads.MIME_TYPE, mediaMimeType)
values.put(MediaStore.Downloads.DATE_ADDED, System.currentTimeMillis())
values.put(MediaStore.Downloads.DATE_TAKEN, System.currentTimeMillis())
}
}
context.contentResolver.insert(externalContentUri, values)?.let { uri ->
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
outputStream.sink().buffer().write(file.inputStream().use { it.readBytes() })
return true
}
}
} else {
@Suppress("DEPRECATION")
Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE).also { mediaScanIntent ->
mediaScanIntent.data = Uri.fromFile(file)
context.sendBroadcast(mediaScanIntent)
}
return true
}
return false
}
/**
* Open the play store to the provided application Id, default to this app
*/

View File

@ -115,6 +115,7 @@ import im.vector.riotx.core.utils.createUIHandler
import im.vector.riotx.core.utils.getColorFromUserId
import im.vector.riotx.core.utils.jsonViewerStyler
import im.vector.riotx.core.utils.openUrlInExternalBrowser
import im.vector.riotx.core.utils.saveMedia
import im.vector.riotx.core.utils.shareMedia
import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.attachments.AttachmentTypeSelectorView
@ -1153,6 +1154,33 @@ class RoomDetailFragment @Inject constructor(
)
}
private fun onSaveActionClicked(action: EventSharedAction.Save) {
session.downloadFile(
FileService.DownloadMode.FOR_EXTERNAL_SHARE,
action.eventId,
action.messageContent.body,
action.messageContent.getFileUrl(),
action.messageContent.encryptedFileInfo?.toElementToDecrypt(),
object : MatrixCallback<File> {
override fun onSuccess(data: File) {
if (isAdded) {
val saved = saveMedia(
context = requireContext(),
file = data,
title = action.messageContent.body,
mediaMimeType = getMimeTypeFromUri(requireContext(), data.toUri())
)
if (saved) {
Toast.makeText(requireContext(), R.string.media_file_added_to_gallery, Toast.LENGTH_LONG).show()
} else {
Toast.makeText(requireContext(), R.string.error_adding_media_file_to_gallery, Toast.LENGTH_LONG).show()
}
}
}
}
)
}
private fun handleActions(action: EventSharedAction) {
when (action) {
is EventSharedAction.OpenUserProfile -> {
@ -1176,6 +1204,9 @@ class RoomDetailFragment @Inject constructor(
is EventSharedAction.Share -> {
onShareActionClicked(action)
}
is EventSharedAction.Save -> {
onSaveActionClicked(action)
}
is EventSharedAction.ViewEditHistory -> {
onEditedDecorationClicked(action.messageInformationData)
}

View File

@ -50,6 +50,9 @@ sealed class EventSharedAction(@StringRes val titleRes: Int,
data class Share(val eventId: String, val messageContent: MessageWithAttachmentContent) :
EventSharedAction(R.string.share, R.drawable.ic_share)
data class Save(val eventId: String, val messageContent: MessageWithAttachmentContent) :
EventSharedAction(R.string.save, R.drawable.ic_material_save)
data class Resend(val eventId: String) :
EventSharedAction(R.string.global_retry, R.drawable.ic_refresh_cw)

View File

@ -30,11 +30,11 @@ import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.isTextMessage
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.MessageFormat
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
@ -290,6 +290,10 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
add(EventSharedAction.Share(timelineEvent.eventId, messageContent))
}
if (canSave(msgType) && messageContent is MessageWithAttachmentContent) {
add(EventSharedAction.Save(timelineEvent.eventId, messageContent))
}
if (timelineEvent.root.sendState == SendState.SENT) {
// TODO Can be redacted
@ -413,4 +417,14 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
else -> false
}
}
private fun canSave(msgType: String?): Boolean {
return when (msgType) {
MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_VIDEO,
MessageType.MSGTYPE_FILE -> true
else -> false
}
}
}

View File

@ -279,6 +279,7 @@ class MessageItemFactory @Inject constructor(
attributes: AbsMessageItem.Attributes): MessageImageVideoItem? {
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val data = ImageContentRenderer.Data(
eventId = informationData.eventId,
filename = messageContent.body,
url = messageContent.getFileUrl(),
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
@ -314,6 +315,7 @@ class MessageItemFactory @Inject constructor(
attributes: AbsMessageItem.Attributes): MessageImageVideoItem? {
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val thumbnailData = ImageContentRenderer.Data(
eventId = informationData.eventId,
filename = messageContent.body,
url = messageContent.videoInfo?.thumbnailFile?.url
?: messageContent.videoInfo?.thumbnailUrl,

View File

@ -46,6 +46,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
@Parcelize
data class Data(
val eventId: String,
val filename: String,
val url: String?,
val elementToDecrypt: ElementToDecrypt?,

View File

@ -21,10 +21,12 @@ import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.view.ViewTreeObserver
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.Toolbar
import androidx.core.net.toUri
import androidx.core.transition.addListener
import androidx.core.view.ViewCompat
import androidx.core.view.isInvisible
@ -36,15 +38,23 @@ 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.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.file.FileService
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.intent.getMimeTypeFromUri
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.utils.shareMedia
import kotlinx.android.synthetic.main.activity_image_media_viewer.*
import timber.log.Timber
import java.io.File
import javax.inject.Inject
class ImageMediaViewerActivity : VectorBaseActivity() {
@Inject lateinit var session: Session
@Inject lateinit var imageContentRenderer: ImageContentRenderer
private lateinit var mediaData: ImageContentRenderer.Data
@ -110,6 +120,33 @@ class ImageMediaViewerActivity : VectorBaseActivity() {
}
}
override fun getMenuRes() = R.menu.vector_media_viewer
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.mediaViewerShareAction -> {
onShareActionClicked()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun onShareActionClicked() {
session.downloadFile(
FileService.DownloadMode.FOR_EXTERNAL_SHARE,
mediaData.eventId,
mediaData.filename,
mediaData.url,
mediaData.elementToDecrypt,
object : MatrixCallback<File> {
override fun onSuccess(data: File) {
shareMedia(this@ImageMediaViewerActivity, data, getMimeTypeFromUri(this@ImageMediaViewerActivity, data.toUri()))
}
}
)
}
private fun configureToolbar(toolbar: Toolbar, mediaData: ImageContentRenderer.Data) {
setSupportActionBar(toolbar)
supportActionBar?.apply {

View File

@ -19,17 +19,29 @@ package im.vector.riotx.features.media
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.widget.Toolbar
import androidx.core.net.toUri
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.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.intent.getMimeTypeFromUri
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.utils.shareMedia
import kotlinx.android.synthetic.main.activity_video_media_viewer.*
import java.io.File
import javax.inject.Inject
class VideoMediaViewerActivity : VectorBaseActivity() {
@Inject lateinit var session: Session
@Inject lateinit var imageContentRenderer: ImageContentRenderer
@Inject lateinit var videoContentRenderer: VideoContentRenderer
private lateinit var mediaData: VideoContentRenderer.Data
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
@ -39,7 +51,7 @@ class VideoMediaViewerActivity : VectorBaseActivity() {
setContentView(im.vector.riotx.R.layout.activity_video_media_viewer)
if (intent.hasExtra(EXTRA_MEDIA_DATA)) {
val mediaData = intent.getParcelableExtra<VideoContentRenderer.Data>(EXTRA_MEDIA_DATA)!!
mediaData = intent.getParcelableExtra<VideoContentRenderer.Data>(EXTRA_MEDIA_DATA)!!
configureToolbar(videoMediaViewerToolbar, mediaData)
imageContentRenderer.render(mediaData.thumbnailMediaData, ImageContentRenderer.Mode.FULL_SIZE, videoMediaViewerThumbnailView)
@ -48,9 +60,38 @@ class VideoMediaViewerActivity : VectorBaseActivity() {
videoMediaViewerLoading,
videoMediaViewerVideoView,
videoMediaViewerErrorView)
} else {
finish()
}
}
override fun getMenuRes() = R.menu.vector_media_viewer
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.mediaViewerShareAction -> {
onShareActionClicked()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun onShareActionClicked() {
session.downloadFile(
FileService.DownloadMode.FOR_EXTERNAL_SHARE,
mediaData.eventId,
mediaData.filename,
mediaData.url,
mediaData.elementToDecrypt,
object : MatrixCallback<File> {
override fun onSuccess(data: File) {
shareMedia(this@VideoMediaViewerActivity, data, getMimeTypeFromUri(this@VideoMediaViewerActivity, data.toUri()))
}
}
)
}
private fun configureToolbar(toolbar: Toolbar, mediaData: VideoContentRenderer.Data) {
setSupportActionBar(toolbar)
supportActionBar?.apply {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 656 B

View File

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/mediaViewerShareAction"
android:icon="@drawable/ic_material_share"
android:title="@string/share"
app:iconTint="?attr/colorAccent"
app:showAsAction="ifRoom" />
</menu>

View File

@ -58,7 +58,8 @@
<!-- BEGIN Strings added by Onuray -->
<string name="media_file_added_to_gallery">Media file added to the Gallery</string>
<string name="error_adding_media_file_to_gallery">Could not add media file to the Gallery</string>
<!-- END Strings added by Onuray -->