mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-25 05:27:42 +00:00
Use AttachmentSaver to save story images.
This commit is contained in:
committed by
Cody Henthorne
parent
293012c219
commit
c876c7847e
@@ -102,7 +102,7 @@ class AttachmentSaver(private val host: Host) {
|
||||
fun dismissSaveProgress()
|
||||
}
|
||||
|
||||
private data class FragmentHost(private val fragment: Fragment) : Host {
|
||||
data class FragmentHost(private val fragment: Fragment) : Host {
|
||||
|
||||
override fun showToast(getMessage: (Context) -> CharSequence) {
|
||||
Toast.makeText(fragment.requireContext(), getMessage(fragment.requireContext()), Toast.LENGTH_LONG).show()
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.AsyncTask
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
@@ -12,12 +11,14 @@ import androidx.core.app.ShareCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.bumptech.glide.load.Options
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.concurrent.SimpleTask
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentSaver
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
@@ -31,7 +32,7 @@ import org.thoughtcrime.securesms.stories.viewer.page.StoryViewerPageState
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil
|
||||
import org.thoughtcrime.securesms.util.DeleteDialog
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentUtil
|
||||
import java.io.ByteArrayInputStream
|
||||
|
||||
object StoryContextMenu {
|
||||
@@ -48,48 +49,46 @@ object StoryContextMenu {
|
||||
).map { (_, deletedThread) -> deletedThread }
|
||||
}
|
||||
|
||||
fun save(context: Context, messageRecord: MessageRecord) {
|
||||
suspend fun save(host: AttachmentSaver.Host, messageRecord: MessageRecord) {
|
||||
val mediaMessageRecord = messageRecord as? MmsMessageRecord
|
||||
val uri: Uri? = mediaMessageRecord?.slideDeck?.firstSlide?.uri
|
||||
val contentType: String? = mediaMessageRecord?.slideDeck?.firstSlide?.contentType
|
||||
|
||||
if (mediaMessageRecord?.storyType?.isTextStory == true) {
|
||||
SimpleTask.run({
|
||||
val model = StoryTextPostModel.parseFrom(messageRecord)
|
||||
val decoder = StoryTextPostModel.Decoder()
|
||||
val bitmap = decoder.decode(model, 1080, 1920, Options()).get()
|
||||
val jpeg: ByteArrayInputStream = BitmapUtil.toCompressedJpeg(bitmap)
|
||||
when {
|
||||
mediaMessageRecord?.storyType?.isTextStory == true -> saveTextStory(host, mediaMessageRecord)
|
||||
uri == null || contentType == null -> showErrorCantSaveStory(host, uri, contentType)
|
||||
else -> saveMediaStory(host, uri, contentType, mediaMessageRecord)
|
||||
}
|
||||
}
|
||||
|
||||
bitmap.recycle()
|
||||
private suspend fun saveTextStory(host: AttachmentSaver.Host, messageRecord: MmsMessageRecord) {
|
||||
val saveAttachment = withContext(Dispatchers.Main) {
|
||||
val model = StoryTextPostModel.parseFrom(messageRecord)
|
||||
val decoder = StoryTextPostModel.Decoder()
|
||||
val bitmap = decoder.decode(model, 1080, 1920, Options()).get()
|
||||
val jpeg: ByteArrayInputStream = BitmapUtil.toCompressedJpeg(bitmap)
|
||||
|
||||
SaveAttachmentTask.Attachment(
|
||||
BlobProvider.getInstance().forData(jpeg.readBytes()).createForSingleUseInMemory(),
|
||||
MediaUtil.IMAGE_JPEG,
|
||||
mediaMessageRecord.dateSent,
|
||||
null
|
||||
)
|
||||
}, { saveAttachment ->
|
||||
SaveAttachmentTask(context)
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, saveAttachment)
|
||||
})
|
||||
return
|
||||
bitmap.recycle()
|
||||
|
||||
SaveAttachmentUtil.SaveAttachment(
|
||||
uri = BlobProvider.getInstance().forData(jpeg.readBytes()).createForSingleUseInMemory(),
|
||||
contentType = MediaUtil.IMAGE_JPEG,
|
||||
date = messageRecord.dateSent,
|
||||
fileName = null
|
||||
)
|
||||
}
|
||||
|
||||
if (uri == null || contentType == null) {
|
||||
Log.w(TAG, "Unable to save story media uri: $uri contentType: $contentType")
|
||||
Toast.makeText(context, R.string.MyStories__unable_to_save, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
AttachmentSaver(host).saveAttachments(setOf(saveAttachment))
|
||||
}
|
||||
|
||||
val saveAttachment = SaveAttachmentTask.Attachment(
|
||||
uri,
|
||||
contentType,
|
||||
mediaMessageRecord.dateSent,
|
||||
null
|
||||
)
|
||||
private suspend fun saveMediaStory(host: AttachmentSaver.Host, uri: Uri, contentType: String, mediaMessageRecord: MmsMessageRecord) {
|
||||
val saveAttachment = SaveAttachmentUtil.SaveAttachment(uri = uri, contentType = contentType, date = mediaMessageRecord.dateSent, fileName = null)
|
||||
AttachmentSaver(host).saveAttachments(setOf(saveAttachment))
|
||||
}
|
||||
|
||||
SaveAttachmentTask(context)
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, saveAttachment)
|
||||
private fun showErrorCantSaveStory(host: AttachmentSaver.Host, uri: Uri?, contentType: String?) {
|
||||
Log.w(TAG, "Unable to save story media uri: $uri contentType: $contentType")
|
||||
host.showToast { context -> context.getString(R.string.MyStories__unable_to_save) }
|
||||
}
|
||||
|
||||
fun share(fragment: Fragment, messageRecord: MmsMessageRecord) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.app.SharedElementCallback
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
|
||||
import androidx.transition.TransitionInflater
|
||||
@@ -23,9 +24,11 @@ import com.google.android.material.snackbar.Snackbar
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentSaver
|
||||
import org.thoughtcrime.securesms.banner.BannerManager
|
||||
import org.thoughtcrime.securesms.banner.banners.DeprecatedBuildBanner
|
||||
import org.thoughtcrime.securesms.banner.banners.UnauthorizedBanner
|
||||
@@ -200,7 +203,13 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
|
||||
.ifNecessary()
|
||||
.onAllGranted { startActivityIfAble(MediaSelectionActivity.camera(requireContext(), isStory = true)) }
|
||||
.withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), R.drawable.symbol_camera_24)
|
||||
.withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos_videos, getParentFragmentManager())
|
||||
.withPermanentDenialDialog(
|
||||
getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos),
|
||||
null,
|
||||
R.string.CameraXFragment_allow_access_camera,
|
||||
R.string.CameraXFragment_to_capture_photos_videos,
|
||||
getParentFragmentManager()
|
||||
)
|
||||
.onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() }
|
||||
.execute()
|
||||
}
|
||||
@@ -320,7 +329,12 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
|
||||
StoryContextMenu.share(this@StoriesLandingFragment, it.data.primaryStory.messageRecord as MmsMessageRecord)
|
||||
},
|
||||
onSave = {
|
||||
StoryContextMenu.save(requireContext(), it.data.primaryStory.messageRecord)
|
||||
lifecycleScope.launch {
|
||||
StoryContextMenu.save(
|
||||
host = AttachmentSaver.FragmentHost(this@StoriesLandingFragment),
|
||||
messageRecord = it.data.primaryStory.messageRecord
|
||||
)
|
||||
}
|
||||
},
|
||||
onDeleteStory = {
|
||||
handleDeleteStory(it)
|
||||
@@ -406,6 +420,7 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("OVERRIDE_DEPRECATION")
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@ import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentSaver
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
@@ -15,6 +18,7 @@ import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
|
||||
import org.thoughtcrime.securesms.stories.StoryTextPostModel
|
||||
@@ -78,7 +82,12 @@ class MyStoriesFragment : DSLSettingsFragment(
|
||||
openStoryViewer(it, preview, false)
|
||||
},
|
||||
onSaveClick = {
|
||||
StoryContextMenu.save(requireContext(), it.distributionStory.messageRecord)
|
||||
lifecycleScope.launch {
|
||||
StoryContextMenu.save(
|
||||
host = AttachmentSaver.FragmentHost(this@MyStoriesFragment),
|
||||
messageRecord = it.distributionStory.messageRecord
|
||||
)
|
||||
}
|
||||
},
|
||||
onDeleteClick = this@MyStoriesFragment::handleDeleteClick,
|
||||
onForwardClick = { item ->
|
||||
@@ -155,4 +164,9 @@ class MyStoriesFragment : DSLSettingsFragment(
|
||||
private fun handleDeleteClick(model: MyStoriesItem.Model) {
|
||||
lifecycleDisposable += StoryContextMenu.delete(requireContext(), setOf(model.distributionStory.messageRecord)).subscribe()
|
||||
}
|
||||
|
||||
@Suppress("OVERRIDE_DEPRECATION")
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import androidx.core.view.animation.PathInterpolatorCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
@@ -40,6 +41,7 @@ import com.google.android.material.snackbar.Snackbar
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.dp
|
||||
@@ -47,6 +49,7 @@ import org.signal.core.util.getParcelableCompat
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentSaver
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
import org.thoughtcrime.securesms.components.segmentedprogressbar.SegmentedProgressBar
|
||||
@@ -63,6 +66,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment
|
||||
import org.thoughtcrime.securesms.mediapreview.VideoControlsDelegate
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment
|
||||
@@ -476,9 +480,11 @@ class StoryViewerPageFragment :
|
||||
state.hideChromeImmediate -> {
|
||||
hideChromeImmediate()
|
||||
}
|
||||
|
||||
state.hideChrome -> {
|
||||
hideChrome()
|
||||
}
|
||||
|
||||
else -> {
|
||||
showChrome()
|
||||
}
|
||||
@@ -493,6 +499,7 @@ class StoryViewerPageFragment :
|
||||
is StoryViewerDialog.GroupDirectReply -> {
|
||||
onStartDirectReply(sheet.storyId, sheet.recipientId)
|
||||
}
|
||||
|
||||
StoryViewerDialog.Delete,
|
||||
StoryViewerDialog.Forward -> Unit
|
||||
}
|
||||
@@ -539,6 +546,11 @@ class StoryViewerPageFragment :
|
||||
viewModel.setIsDisplayingForwardDialog(false)
|
||||
}
|
||||
|
||||
@Suppress("OVERRIDE_DEPRECATION")
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
private fun checkEventIntersectsClickableSpan(cardWrapper: ViewGroup, event: MotionEvent): Boolean {
|
||||
if (viewModel.getPost()?.content?.isText() != true) {
|
||||
textStoryIntersectProcessingEvents = false
|
||||
@@ -712,12 +724,14 @@ class StoryViewerPageFragment :
|
||||
constraintSet.connect(viewsAndReplies.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
|
||||
card.radius = DimensionUnit.DP.toPixels(18f)
|
||||
}
|
||||
|
||||
StoryDisplay.MEDIUM -> {
|
||||
constraintSet.setDimensionRatio(cardWrapper.id, "9:16")
|
||||
constraintSet.clear(viewsAndReplies.id, ConstraintSet.TOP)
|
||||
constraintSet.connect(viewsAndReplies.id, ConstraintSet.BOTTOM, cardWrapper.id, ConstraintSet.BOTTOM)
|
||||
card.radius = DimensionUnit.DP.toPixels(18f)
|
||||
}
|
||||
|
||||
StoryDisplay.SMALL -> {
|
||||
constraintSet.setDimensionRatio(cardWrapper.id, null)
|
||||
constraintSet.clear(viewsAndReplies.id, ConstraintSet.TOP)
|
||||
@@ -757,6 +771,7 @@ class StoryViewerPageFragment :
|
||||
isFromNotification,
|
||||
groupReplyStartPosition
|
||||
)
|
||||
|
||||
StoryViewerPageState.ReplyState.PRIVATE -> StoryDirectReplyDialogFragment.create(storyPostId)
|
||||
StoryViewerPageState.ReplyState.GROUP_SELF -> StoryViewsAndRepliesDialogFragment.create(
|
||||
storyPostId,
|
||||
@@ -765,14 +780,17 @@ class StoryViewerPageFragment :
|
||||
isFromNotification,
|
||||
groupReplyStartPosition
|
||||
)
|
||||
|
||||
StoryViewerPageState.ReplyState.PARTIAL_SEND -> {
|
||||
handleResend(storyPost)
|
||||
return
|
||||
}
|
||||
|
||||
StoryViewerPageState.ReplyState.SEND_FAILURE -> {
|
||||
handleResend(storyPost)
|
||||
return
|
||||
}
|
||||
|
||||
StoryViewerPageState.ReplyState.SENDING -> return
|
||||
}
|
||||
|
||||
@@ -854,24 +872,28 @@ class StoryViewerPageFragment :
|
||||
viewModel.setIsDisplayingSlate(false)
|
||||
markViewedIfAble()
|
||||
}
|
||||
|
||||
AttachmentTable.TRANSFER_PROGRESS_PENDING -> {
|
||||
Log.d(TAG, "Story content download is pending.")
|
||||
storySlate.moveToState(StorySlateView.State.LOADING, post.id)
|
||||
sharedViewModel.setContentIsReady()
|
||||
viewModel.setIsDisplayingSlate(true)
|
||||
}
|
||||
|
||||
AttachmentTable.TRANSFER_PROGRESS_STARTED -> {
|
||||
Log.d(TAG, "Story content download is in progress.")
|
||||
storySlate.moveToState(StorySlateView.State.LOADING, post.id)
|
||||
sharedViewModel.setContentIsReady()
|
||||
viewModel.setIsDisplayingSlate(true)
|
||||
}
|
||||
|
||||
AttachmentTable.TRANSFER_PROGRESS_FAILED -> {
|
||||
Log.d(TAG, "Story content download has failed temporarily.")
|
||||
storySlate.moveToState(StorySlateView.State.ERROR, post.id)
|
||||
sharedViewModel.setContentIsReady()
|
||||
viewModel.setIsDisplayingSlate(true)
|
||||
}
|
||||
|
||||
AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE -> {
|
||||
Log.d(TAG, "Story content download has failed permanently.")
|
||||
storySlate.moveToState(StorySlateView.State.FAILED, post.id, post.sender)
|
||||
@@ -1175,7 +1197,12 @@ class StoryViewerPageFragment :
|
||||
StoryContextMenu.share(this, it.conversationMessage.messageRecord as MmsMessageRecord)
|
||||
},
|
||||
onSave = {
|
||||
StoryContextMenu.save(requireContext(), it.conversationMessage.messageRecord)
|
||||
lifecycleScope.launch {
|
||||
StoryContextMenu.save(
|
||||
host = AttachmentSaver.FragmentHost(this@StoryViewerPageFragment),
|
||||
messageRecord = it.conversationMessage.messageRecord
|
||||
)
|
||||
}
|
||||
},
|
||||
onDelete = {
|
||||
viewModel.setIsDisplayingDeleteDialog(true)
|
||||
|
||||
Reference in New Issue
Block a user