mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-03-15 18:59:12 +03:00
Merge pull request #1242 from vector-im/feature/save_media_to_gallery
Save media files to Gallery
This commit is contained in:
commit
5795b7e063
13 changed files with 206 additions and 3 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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?,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 |
5
vector/src/main/res/drawable/ic_material_save.xml
Normal file
5
vector/src/main/res/drawable/ic_material_save.xml
Normal 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>
|
10
vector/src/main/res/menu/vector_media_viewer.xml
Normal file
10
vector/src/main/res/menu/vector_media_viewer.xml
Normal 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>
|
|
@ -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 -->
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue