Tidy up MediaSendFragment (#1068)

pull/1713/head
SessionHero01 3 weeks ago committed by GitHub
parent f2cf7565e5
commit dbcffbd2b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -107,7 +107,6 @@ import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.ListenableFuture
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.utilities.hexEncodedPrivateKey
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.ScreenLockActionBarActivity
@ -197,7 +196,6 @@ import org.thoughtcrime.securesms.util.isFullyScrolled
import org.thoughtcrime.securesms.util.isScrolledToBottom
import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom
import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.scrollAmount
import org.thoughtcrime.securesms.util.show
import org.thoughtcrime.securesms.util.toPx
import org.thoughtcrime.securesms.webrtc.WebRtcCallActivity
@ -825,7 +823,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
AttachmentManager.MediaType.GIF == mediaType ||
AttachmentManager.MediaType.VIDEO == mediaType)
) {
val media = Media(mediaURI, filename, mimeType, 0, 0, 0, 0, Optional.absent(), Optional.absent())
val media = Media(mediaURI, filename, mimeType, 0, 0, 0, 0, null, null)
startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), viewModel.recipient!!, ""), PICK_FROM_LIBRARY)
return
} else {
@ -1908,7 +1906,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
val recipient = viewModel.recipient ?: return
val mimeType = MediaUtil.getMimeType(this, contentUri)!!
val filename = FilenameUtils.getFilenameFromUri(this, contentUri, mimeType)
val media = Media(contentUri, filename, mimeType, 0, 0, 0, 0, Optional.absent(), Optional.absent())
val media = Media(contentUri, filename, mimeType, 0, 0, 0, 0, null, null)
startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), recipient, getMessageBody()), PICK_FROM_LIBRARY)
}
@ -2121,9 +2119,9 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
for (media in mediaList) {
val mediaFilename: String? = media.filename
when {
MediaUtil.isVideoType(media.mimeType) -> { slideDeck.addSlide(VideoSlide(this, media.uri, mediaFilename, 0, media.caption.orNull())) }
MediaUtil.isGif(media.mimeType) -> { slideDeck.addSlide(GifSlide(this, media.uri, mediaFilename, 0, media.width, media.height, media.caption.orNull())) }
MediaUtil.isImageType(media.mimeType) -> { slideDeck.addSlide(ImageSlide(this, media.uri, mediaFilename, 0, media.width, media.height, media.caption.orNull())) }
MediaUtil.isVideoType(media.mimeType) -> { slideDeck.addSlide(VideoSlide(this, media.uri, mediaFilename, 0, media.caption)) }
MediaUtil.isGif(media.mimeType) -> { slideDeck.addSlide(GifSlide(this, media.uri, mediaFilename, 0, media.width, media.height, media.caption)) }
MediaUtil.isImageType(media.mimeType) -> { slideDeck.addSlide(ImageSlide(this, media.uri, mediaFilename, 0, media.width, media.height, media.caption)) }
else -> {
Log.d(TAG, "Asked to send an unexpected media type: '" + media.mimeType + "'. Skipping.")
}

@ -562,7 +562,9 @@ public class AttachmentDatabase extends Database {
throw new MmsException("No attachment data found!");
}
dataInfo = setAttachmentData(dataInfo.file, mediaStream.getStream());
final File oldFile = dataInfo.file;
dataInfo = setAttachmentData(mediaStream.getStream());
ContentValues contentValues = new ContentValues();
contentValues.put(SIZE, dataInfo.length);
@ -570,9 +572,18 @@ public class AttachmentDatabase extends Database {
contentValues.put(WIDTH, mediaStream.getWidth());
contentValues.put(HEIGHT, mediaStream.getHeight());
contentValues.put(DATA_RANDOM, dataInfo.random);
contentValues.put(DATA, dataInfo.file.getAbsolutePath());
database.update(TABLE_NAME, contentValues, PART_ID_WHERE, databaseAttachment.getAttachmentId().toStrings());
if (oldFile != null && oldFile.exists()) {
try {
oldFile.delete();
} catch (Exception e) {
Log.w(TAG, "Error deleting an old attachment file", e);
}
}
return new DatabaseAttachment(databaseAttachment.getAttachmentId(),
databaseAttachment.getMmsId(),
databaseAttachment.hasData(),
@ -696,20 +707,12 @@ public class AttachmentDatabase extends Database {
try {
File partsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE);
File dataFile = File.createTempFile("part", ".mms", partsDirectory);
return setAttachmentData(dataFile, in);
} catch (IOException e) {
throw new MmsException(e);
}
}
private @NonNull DataInfo setAttachmentData(@NonNull File destination, @NonNull InputStream in)
throws MmsException
{
try {
Pair<byte[], OutputStream> out = ModernEncryptingPartOutputStream.createFor(attachmentSecret, destination, false);
Log.d("AttachmentDatabase", "Writing attachment data to: " + dataFile.getAbsolutePath());
Pair<byte[], OutputStream> out = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false);
long length = Util.copy(in, out.second);
return new DataInfo(destination, length, out.first);
return new DataInfo(dataFile, length, out.first);
} catch (IOException e) {
throw new MmsException(e);
}

@ -119,8 +119,8 @@ public class MediaPreviewViewModel extends ViewModel {
mediaRecord.getAttachment().getWidth(),
mediaRecord.getAttachment().getHeight(),
mediaRecord.getAttachment().getSize(),
Optional.absent(),
Optional.fromNullable(mediaRecord.getAttachment().getCaption())
null,
mediaRecord.getAttachment().getCaption()
);
}

@ -1,95 +0,0 @@
package org.thoughtcrime.securesms.mediasend;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import org.session.libsignal.utilities.guava.Optional;
/**
* Represents a piece of media that the user has on their device.
*/
public class Media implements Parcelable {
public static final String ALL_MEDIA_BUCKET_ID = "org.thoughtcrime.securesms.ALL_MEDIA";
private final Uri uri;
private final String filename;
private final String mimeType;
private final long date;
private final int width;
private final int height;
private final long size;
private final Optional<String> bucketId;
private Optional<String> caption;
public Media(@NonNull Uri uri, @NonNull String filename, @NonNull String mimeType, long date, int width, int height, long size, Optional<String> bucketId, Optional<String> caption) {
this.uri = uri;
this.filename = filename;
this.mimeType = mimeType;
this.date = date;
this.width = width;
this.height = height;
this.size = size;
this.bucketId = bucketId;
this.caption = caption;
}
protected Media(Parcel in) {
uri = in.readParcelable(Uri.class.getClassLoader());
filename = in.readString();
mimeType = in.readString();
date = in.readLong();
width = in.readInt();
height = in.readInt();
size = in.readLong();
bucketId = Optional.fromNullable(in.readString());
caption = Optional.fromNullable(in.readString());
}
public Uri getUri() { return uri; }
public String getFilename() { return filename; }
public String getMimeType() { return mimeType; }
public long getDate() { return date; }
public int getWidth() { return width; }
public int getHeight() { return height; }
public long getSize() { return size; }
public Optional<String> getBucketId() { return bucketId; }
public Optional<String> getCaption() { return caption; }
public void setCaption(String caption) { this.caption = Optional.fromNullable(caption); }
@Override
public int describeContents() { return 0; }
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeParcelable(uri, flags);
dest.writeString(filename);
dest.writeString(mimeType);
dest.writeLong(date);
dest.writeInt(width);
dest.writeInt(height);
dest.writeLong(size);
dest.writeString(bucketId.orNull());
dest.writeString(caption.orNull());
}
public static final Creator<Media> CREATOR = new Creator<Media>() {
@Override
public Media createFromParcel(Parcel in) { return new Media(in); }
@Override
public Media[] newArray(int size) { return new Media[size]; }
};
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Media media = (Media)o;
return uri.equals(media.uri);
}
@Override
public int hashCode() { return uri.hashCode(); }
}

@ -0,0 +1,49 @@
package org.thoughtcrime.securesms.mediasend
import android.net.Uri
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* Represents a piece of media that the user has on their device.
*/
@Parcelize
data class Media(
val uri: Uri,
val filename: String,
val mimeType: String,
val date: Long,
val width: Int,
val height: Int,
val size: Long,
val bucketId: String?,
val caption: String?,
) : Parcelable {
// The equality check here is performed based only on the URI of the media.
// This behavior very opinionated and shouldn't really be in a generic equality check in the first place.
// However there are too much code working under this assumption and we can't simply change it to
// a generic solution.
//
// To later dev: once sufficient refactors are done, we can remove this equality
// check and rely on the data class default equality check instead.
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Media) return false
if (uri != other.uri) return false
return true
}
override fun hashCode(): Int {
return uri.hashCode()
}
companion object {
const val ALL_MEDIA_BUCKET_ID: String = "org.thoughtcrime.securesms.ALL_MEDIA"
}
}

@ -194,7 +194,7 @@ class MediaRepository {
long size = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.SIZE));
String filename = cursor.getString(cursor.getColumnIndexOrThrow(Images.Media.DISPLAY_NAME));
media.add(new Media(uri, filename, mimetype, date, width, height, size, Optional.of(bucketId), Optional.absent()));
media.add(new Media(uri, filename, mimetype, date, width, height, size, bucketId, null));
}
}

@ -254,8 +254,8 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme
width,
height,
data.size.toLong(),
Optional.of<String>(Media.ALL_MEDIA_BUCKET_ID),
Optional.absent<String>()
Media.ALL_MEDIA_BUCKET_ID,
null
)
} catch (e: Exception) {
return@run null

@ -1,11 +1,9 @@
package org.thoughtcrime.securesms.mediasend
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Rect
import android.net.Uri
import android.os.AsyncTask
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
@ -17,46 +15,39 @@ import android.view.ViewGroup
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.widget.ImageButton
import android.widget.TextView
import android.widget.TextView.OnEditorActionListener
import androidx.core.os.BundleCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager.SimpleOnPageChangeListener
import com.bumptech.glide.Glide
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withContext
import network.loki.messenger.R
import org.session.libsession.utilities.Address
import network.loki.messenger.databinding.MediasendFragmentBinding
import org.session.libsession.utilities.MediaTypes
import org.session.libsession.utilities.TextSecurePreferences.Companion.isEnterSendsEnabled
import org.session.libsession.utilities.Util.cancelRunnableOnMain
import org.session.libsession.utilities.Util.isEmpty
import org.session.libsession.utilities.Util.runOnMainDelayed
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.ListenableFuture
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.SettableFuture
import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.components.ComposeText
import org.thoughtcrime.securesms.components.ControllableViewPager
import org.thoughtcrime.securesms.components.InputAwareLayout
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardHiddenListener
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener
import org.thoughtcrime.securesms.imageeditor.model.EditorModel
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter.RailItemListener
import org.thoughtcrime.securesms.mediasend.MediaSendViewModel
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment
import org.thoughtcrime.securesms.util.PushCharacterCalculator
import org.thoughtcrime.securesms.util.Stopwatch
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.Locale
import java.util.concurrent.ExecutionException
/**
* Allows the user to edit and caption a set of media items before choosing to send them.
@ -64,35 +55,24 @@ import java.util.concurrent.ExecutionException
@AndroidEntryPoint
class MediaSendFragment : Fragment(), OnGlobalLayoutListener, RailItemListener,
OnKeyboardShownListener, OnKeyboardHiddenListener {
private var hud: InputAwareLayout? = null
private var captionAndRail: View? = null
private var sendButton: ImageButton? = null
private var composeText: ComposeText? = null
private var composeContainer: ViewGroup? = null
private var playbackControlsContainer: ViewGroup? = null
private var charactersLeft: TextView? = null
private var closeButton: View? = null
private var loader: View? = null
private var fragmentPager: ControllableViewPager? = null
private var binding: MediasendFragmentBinding? = null
private var fragmentPagerAdapter: MediaSendFragmentPagerAdapter? = null
private var mediaRail: RecyclerView? = null
private var mediaRailAdapter: MediaRailAdapter? = null
private var visibleHeight = 0
private var viewModel: MediaSendViewModel? = null
private var controller: Controller? = null
private val visibleBounds = Rect()
private val characterCalculator = PushCharacterCalculator()
private val controller: Controller
get() = (parentFragment as? Controller) ?: requireActivity() as Controller
override fun onAttach(context: Context) {
super.onAttach(context)
check(requireActivity() is Controller) { "Parent activity must implement controller interface." }
controller = requireActivity() as Controller
viewModel = ViewModelProvider(requireActivity()).get(
MediaSendViewModel::class.java
)
@ -102,8 +82,8 @@ class MediaSendFragment : Fragment(), OnGlobalLayoutListener, RailItemListener,
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.mediasend_fragment, container, false)
): View {
return MediasendFragmentBinding.inflate(inflater, container, false).root
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -112,83 +92,76 @@ class MediaSendFragment : Fragment(), OnGlobalLayoutListener, RailItemListener,
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
hud = view.findViewById(R.id.mediasend_hud)
captionAndRail = view.findViewById(R.id.mediasend_caption_and_rail)
sendButton = view.findViewById(R.id.mediasend_send_button)
composeText = view.findViewById(R.id.mediasend_compose_text)
composeContainer = view.findViewById(R.id.mediasend_compose_container)
fragmentPager = view.findViewById(R.id.mediasend_pager)
mediaRail = view.findViewById(R.id.mediasend_media_rail)
playbackControlsContainer = view.findViewById(R.id.mediasend_playback_controls_container)
charactersLeft = view.findViewById(R.id.mediasend_characters_left)
closeButton = view.findViewById(R.id.mediasend_close_button)
loader = view.findViewById(R.id.loader)
val sendButtonBkg = view.findViewById<View>(R.id.mediasend_send_button_bkg)
sendButton!!.setOnClickListener(View.OnClickListener { v: View? ->
if (hud!!.isKeyboardOpen()) {
hud!!.hideSoftkey(composeText, null)
val binding = MediasendFragmentBinding.bind(view).also {
this.binding = it
}
binding.mediasendSendButton.setOnClickListener { v: View? ->
if (binding.mediasendHud.isKeyboardOpen) {
binding.mediasendHud.hideSoftkey(binding.mediasendComposeText, null)
}
processMedia(fragmentPagerAdapter!!.allMedia, fragmentPagerAdapter!!.savedState)
})
fragmentPagerAdapter?.let { processMedia(it.allMedia, it.savedState) }
}
val composeKeyPressedListener = ComposeKeyPressedListener()
composeText!!.setOnKeyListener(composeKeyPressedListener)
composeText!!.addTextChangedListener(composeKeyPressedListener)
composeText!!.setOnClickListener(composeKeyPressedListener)
composeText!!.setOnFocusChangeListener(composeKeyPressedListener)
binding.mediasendComposeText.setOnKeyListener(composeKeyPressedListener)
binding.mediasendComposeText.addTextChangedListener(composeKeyPressedListener)
binding.mediasendComposeText.setOnClickListener(composeKeyPressedListener)
binding.mediasendComposeText.setOnFocusChangeListener(composeKeyPressedListener)
composeText!!.requestFocus()
binding.mediasendComposeText.requestFocus()
fragmentPagerAdapter = MediaSendFragmentPagerAdapter(childFragmentManager)
fragmentPager!!.setAdapter(fragmentPagerAdapter)
binding.mediasendPager.setAdapter(fragmentPagerAdapter)
val pageChangeListener = FragmentPageChangeListener()
fragmentPager!!.addOnPageChangeListener(pageChangeListener)
fragmentPager!!.post(Runnable { pageChangeListener.onPageSelected(fragmentPager!!.currentItem) })
binding.mediasendPager.addOnPageChangeListener(pageChangeListener)
binding.mediasendPager.post(Runnable { pageChangeListener.onPageSelected(binding.mediasendPager.currentItem) })
mediaRailAdapter = MediaRailAdapter(Glide.with(this), this, true)
mediaRail!!.setLayoutManager(
binding.mediasendMediaRail.setLayoutManager(
LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
)
mediaRail!!.setAdapter(mediaRailAdapter)
binding.mediasendMediaRail.setAdapter(mediaRailAdapter)
hud!!.getRootView().viewTreeObserver.addOnGlobalLayoutListener(this)
hud!!.addOnKeyboardShownListener(this)
hud!!.addOnKeyboardHiddenListener(this)
binding.mediasendHud.getRootView().viewTreeObserver.addOnGlobalLayoutListener(this)
binding.mediasendHud.addOnKeyboardShownListener(this)
binding.mediasendHud.addOnKeyboardHiddenListener(this)
composeText!!.append(viewModel!!.body)
binding.mediasendComposeText.append(viewModel?.body)
val recipient = Recipient.from(
requireContext(),
arguments!!.getParcelable(KEY_ADDRESS)!!, false
)
val displayName = Optional.fromNullable(recipient.name)
.or(
Optional.fromNullable(recipient.profileName)
.or(recipient.address.toString())
)
composeText!!.setHint(getString(R.string.message), null)
composeText!!.setOnEditorActionListener(OnEditorActionListener { v: TextView?, actionId: Int, event: KeyEvent? ->
binding.mediasendComposeText.setHint(getString(R.string.message), null)
binding.mediasendComposeText.setOnEditorActionListener { v: TextView?, actionId: Int, event: KeyEvent? ->
val isSend = actionId == EditorInfo.IME_ACTION_SEND
if (isSend) sendButton!!.performClick()
if (isSend) binding.mediasendSendButton.performClick()
isSend
})
}
closeButton!!.setOnClickListener(View.OnClickListener { v: View? -> requireActivity().onBackPressed() })
binding.mediasendCloseButton.setOnClickListener { requireActivity().onBackPressed() }
}
override fun onDestroyView() {
super.onDestroyView()
binding = null
}
override fun onStart() {
super.onStart()
fragmentPagerAdapter!!.restoreState(viewModel!!.drawState)
viewModel!!.onImageEditorStarted()
val viewModel = viewModel
val adapter = fragmentPagerAdapter
if (viewModel != null && adapter != null) {
adapter.restoreState(viewModel.drawState)
viewModel.onImageEditorStarted()
}
requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
@ -200,216 +173,262 @@ class MediaSendFragment : Fragment(), OnGlobalLayoutListener, RailItemListener,
override fun onStop() {
super.onStop()
fragmentPagerAdapter!!.saveAllState()
viewModel!!.saveDrawState(fragmentPagerAdapter!!.savedState)
val viewModel = viewModel
val adapter = fragmentPagerAdapter
if (viewModel != null && adapter != null) {
adapter.saveAllState()
viewModel.saveDrawState(adapter.savedState)
}
}
override fun onGlobalLayout() {
hud!!.rootView.getWindowVisibleDisplayFrame(visibleBounds)
val hud = binding?.mediasendHud ?: return
hud.rootView.getWindowVisibleDisplayFrame(visibleBounds)
val currentVisibleHeight = visibleBounds.height()
if (currentVisibleHeight != visibleHeight) {
hud!!.layoutParams.height = currentVisibleHeight
hud!!.layout(
hud.layoutParams.height = currentVisibleHeight
hud.layout(
visibleBounds.left,
visibleBounds.top,
visibleBounds.right,
visibleBounds.bottom
)
hud!!.requestLayout()
hud.requestLayout()
visibleHeight = currentVisibleHeight
}
}
override fun onRailItemClicked(distanceFromActive: Int) {
viewModel!!.onPageChanged(fragmentPager!!.currentItem + distanceFromActive)
val currentItem = binding?.mediasendPager?.currentItem ?: return
viewModel?.onPageChanged(currentItem + distanceFromActive)
}
override fun onRailItemDeleteClicked(distanceFromActive: Int) {
viewModel!!.onMediaItemRemoved(
val currentItem = binding?.mediasendPager?.currentItem ?: return
viewModel?.onMediaItemRemoved(
requireContext(),
fragmentPager!!.currentItem + distanceFromActive
currentItem + distanceFromActive
)
}
override fun onKeyboardShown() {
if (composeText!!.hasFocus()) {
mediaRail!!.visibility = View.VISIBLE
composeContainer!!.visibility = View.VISIBLE
val binding = binding ?: return
if (binding.mediasendComposeText.hasFocus()) {
binding.mediasendMediaRail.visibility = View.VISIBLE
binding.mediasendComposeContainer.visibility = View.VISIBLE
} else {
mediaRail!!.visibility = View.GONE
composeContainer!!.visibility = View.VISIBLE
binding.mediasendMediaRail.visibility = View.GONE
binding.mediasendComposeContainer.visibility = View.VISIBLE
}
}
override fun onKeyboardHidden() {
composeContainer!!.visibility = View.VISIBLE
mediaRail!!.visibility = View.VISIBLE
binding?.apply {
mediasendComposeContainer.visibility = View.VISIBLE
mediasendMediaRail.visibility = View.VISIBLE
}
}
fun onTouchEventsNeeded(needed: Boolean) {
if (fragmentPager != null) {
fragmentPager!!.isEnabled = !needed
}
binding?.mediasendPager?.isEnabled = !needed
}
fun handleBackPress(): Boolean {
if (hud!!.isInputOpen) {
hud!!.hideCurrentInput(composeText)
val hud = binding?.mediasendHud ?: return false
val composeText = binding?.mediasendComposeText ?: return false
if (hud.isInputOpen) {
hud.hideCurrentInput(composeText)
return true
}
return false
}
private fun initViewModel() {
viewModel!!.getSelectedMedia().observe(
val viewModel = requireNotNull(viewModel) {
"ViewModel is not initialized"
}
viewModel.getSelectedMedia().observe(
this
) { media: List<Media?>? ->
if (isEmpty(media)) {
controller!!.onNoMediaAvailable()
if (media.isNullOrEmpty()) {
controller.onNoMediaAvailable()
return@observe
}
fragmentPagerAdapter!!.setMedia(media!!)
mediaRail!!.visibility = View.VISIBLE
mediaRailAdapter!!.setMedia(media)
fragmentPagerAdapter?.setMedia(media)
binding?.mediasendMediaRail?.visibility = View.VISIBLE
mediaRailAdapter?.setMedia(media)
}
viewModel!!.getPosition().observe(this) { position: Int? ->
viewModel.getPosition().observe(this) { position: Int? ->
if (position == null || position < 0) return@observe
fragmentPager!!.setCurrentItem(position, true)
mediaRailAdapter!!.setActivePosition(position)
mediaRail!!.smoothScrollToPosition(position)
binding?.mediasendPager?.setCurrentItem(position, true)
mediaRailAdapter?.setActivePosition(position)
binding?.mediasendMediaRail?.smoothScrollToPosition(position)
val playbackControls = fragmentPagerAdapter!!.getPlaybackControls(position)
val playbackControls = fragmentPagerAdapter?.getPlaybackControls(position)
if (playbackControls != null) {
val params = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
playbackControls.layoutParams = params
playbackControlsContainer!!.removeAllViews()
playbackControlsContainer!!.addView(playbackControls)
binding?.mediasendPlaybackControlsContainer?.removeAllViews()
binding?.mediasendPlaybackControlsContainer?.addView(playbackControls)
} else {
playbackControlsContainer!!.removeAllViews()
binding?.mediasendPlaybackControlsContainer?.removeAllViews()
}
}
viewModel!!.getBucketId().observe(this) { bucketId: String? ->
viewModel.getBucketId().observe(this) { bucketId: String? ->
if (bucketId == null) return@observe
mediaRailAdapter!!.setAddButtonListener { controller!!.onAddMediaClicked(bucketId) }
mediaRailAdapter!!.setAddButtonListener { controller.onAddMediaClicked(bucketId) }
}
}
private fun presentCharactersRemaining() {
val messageBody = composeText!!.textTrimmed
val binding = binding ?: return
val messageBody = binding.mediasendComposeText.textTrimmed
val characterState = characterCalculator.calculateCharacters(messageBody)
if (characterState.charactersRemaining <= 15 || characterState.messagesSpent > 1) {
charactersLeft!!.text = String.format(
binding.mediasendCharactersLeft.text = String.format(
Locale.getDefault(),
"%d/%d (%d)",
characterState.charactersRemaining,
characterState.maxTotalMessageSize,
characterState.messagesSpent
)
charactersLeft!!.visibility = View.VISIBLE
binding.mediasendCharactersLeft.visibility = View.VISIBLE
} else {
charactersLeft!!.visibility = View.GONE
binding.mediasendCharactersLeft.visibility = View.GONE
}
}
@SuppressLint("StaticFieldLeak")
private fun processMedia(mediaList: List<Media>, savedState: Map<Uri, Any>) {
val futures: MutableMap<Media, ListenableFuture<Bitmap>> = HashMap()
val binding = binding ?: return // If the view is destroyed, this process should not continue
for (media in mediaList) {
val state = savedState[media.uri]
val context = requireContext().applicationContext
if (state is ImageEditorFragment.Data) {
val model = state.readModel()
if (model != null && model.isChanged) {
futures[media] = render(requireContext(), model)
}
}
}
object : AsyncTask<Void?, Void?, List<Media>>() {
private var renderTimer: Stopwatch? = null
private var progressTimer: Runnable? = null
override fun onPreExecute() {
renderTimer = Stopwatch("ProcessMedia")
progressTimer = Runnable {
loader!!.visibility = View.VISIBLE
}
runOnMainDelayed(progressTimer!!, 250)
lifecycleScope.launch {
val delayedShowLoader = launch {
delay(250)
binding.loader.isVisible = true
}
override fun doInBackground(vararg params: Void?): List<Media> {
val context = requireContext()
val updatedMedia: MutableList<Media> = ArrayList(mediaList.size)
for (media in mediaList) {
if (futures.containsKey(media)) {
try {
val bitmap = futures[media]!!.get()
val baos = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos)
val uri = BlobProvider.getInstance()
.forData(baos.toByteArray())
.withMimeType(MediaTypes.IMAGE_JPEG)
.createForSingleSessionOnDisk(
context
) { e: IOException? ->
Log.w(
TAG,
"Failed to write to disk.",
e
)
val updatedMedia = supervisorScope {
// For each media, render the image in the background if necessary
val renderingTasks = mediaList
.asSequence()
.map { media ->
media to (savedState[media.uri] as? ImageEditorFragment.Data)
?.readModel()
?.takeIf { it.isChanged }
}
.associate { (media, model) ->
media.uri to async {
runCatching {
if (model != null) {
// While we render the bitmap in the background, make sure
// we limit the number of parallel tasks to avoid overwhelming the memory,
// as bitmaps are memory intensive.
withContext(Dispatchers.Default.limitedParallelism(2)) {
val bitmap = model.render(context)
try {
// Compress the bitmap to JPEG
val jpegOut = requireNotNull(
File.createTempFile(
"media_preview",
".jpg",
context.cacheDir
)
) {
"Unable to create temporary file"
}
val (jpegSize, uri) = try {
FileOutputStream(jpegOut).use { out ->
bitmap.compress(
Bitmap.CompressFormat.JPEG,
80,
out
)
}
// Once we have the JPEG file, save it as our blob
val jpegSize = jpegOut.length()
jpegSize to BlobProvider.getInstance()
.forData(FileInputStream(jpegOut), jpegSize)
.withMimeType(MediaTypes.IMAGE_JPEG)
.withFileName(media.filename)
.createForSingleSessionOnDisk(context, null)
.await()
} finally {
// Clean up the temporary file
jpegOut.delete()
}
media.copy(
uri = uri,
mimeType = MediaTypes.IMAGE_JPEG,
width = bitmap.width,
height = bitmap.height,
size = jpegSize,
)
} finally {
bitmap.recycle()
}
}
} else {
// No changes to the original media, copy and return as is
val newUri = BlobProvider.getInstance()
.forData(requireNotNull(context.contentResolver.openInputStream(media.uri)) {
"Invalid URI"
}, media.size)
.withMimeType(media.mimeType)
.withFileName(media.filename)
.createForSingleSessionOnDisk(context, null)
.await()
media.copy(uri = newUri)
}
.get()
val updated = Media(
uri,
media.filename,
MediaTypes.IMAGE_JPEG,
media.date,
bitmap.width,
bitmap.height,
baos.size().toLong(),
media.bucketId,
media.caption
)
updatedMedia.add(updated)
renderTimer!!.split("item")
} catch (e: Exception) {
Log.w(TAG, "Failed to render image. Using base image.", e)
updatedMedia.add(media)
}
}
} else {
updatedMedia.add(media)
}
// For each media, if there's a rendered version, use that or keep the original
mediaList.map { media ->
renderingTasks[media.uri]?.await()?.let { rendered ->
if (rendered.isFailure) {
Log.w(TAG, "Error rendering image", rendered.exceptionOrNull())
media
} else {
rendered.getOrThrow()
}
} ?: media
}
return updatedMedia
}
override fun onPostExecute(media: List<Media>) {
controller!!.onSendClicked(media, composeText!!.textTrimmed)
cancelRunnableOnMain(progressTimer!!)
loader!!.visibility = View.GONE
renderTimer!!.stop(TAG)
}
}.execute()
controller.onSendClicked(updatedMedia, binding.mediasendComposeText.textTrimmed)
delayedShowLoader.cancel()
binding.loader.isVisible = false
}
}
fun onRequestFullScreen(fullScreen: Boolean) {
captionAndRail!!.visibility =
binding?.mediasendCaptionAndRail?.visibility =
if (fullScreen) View.GONE else View.VISIBLE
}
@ -427,13 +446,13 @@ class MediaSendFragment : Fragment(), OnGlobalLayoutListener, RailItemListener,
if (event.action == KeyEvent.ACTION_DOWN) {
if (keyCode == KeyEvent.KEYCODE_ENTER) {
if (isEnterSendsEnabled(requireContext())) {
sendButton!!.dispatchKeyEvent(
binding?.mediasendSendButton?.dispatchKeyEvent(
KeyEvent(
KeyEvent.ACTION_DOWN,
KeyEvent.KEYCODE_ENTER
)
)
sendButton!!.dispatchKeyEvent(
binding?.mediasendSendButton?.dispatchKeyEvent(
KeyEvent(
KeyEvent.ACTION_UP,
KeyEvent.KEYCODE_ENTER
@ -447,11 +466,12 @@ class MediaSendFragment : Fragment(), OnGlobalLayoutListener, RailItemListener,
}
override fun onClick(v: View) {
hud!!.showSoftkey(composeText)
val binding = binding ?: return
binding.mediasendHud.showSoftkey(binding.mediasendComposeText)
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
beforeLength = composeText!!.textTrimmed.length
beforeLength = binding?.mediasendComposeText?.textTrimmed?.length ?: return
}
override fun afterTextChanged(s: Editable) {
@ -483,13 +503,5 @@ class MediaSendFragment : Fragment(), OnGlobalLayoutListener, RailItemListener,
fragment.arguments = args
return fragment
}
private fun render(context: Context, model: EditorModel): ListenableFuture<Bitmap> {
val future = SettableFuture<Bitmap>()
AsyncTask.THREAD_POOL_EXECUTOR.execute { future.set(model.render(context)) }
return future
}
}
}

@ -17,6 +17,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import kotlin.collections.CollectionsKt;
class MediaSendFragmentPagerAdapter extends FragmentStatePagerAdapter {
private final List<Media> media;
@ -83,7 +85,7 @@ class MediaSendFragmentPagerAdapter extends FragmentStatePagerAdapter {
}
List<Media> getAllMedia() {
return media;
return CollectionsKt.toList(media);
}
void setMedia(@NonNull List<Media> media) {

@ -82,9 +82,9 @@ internal class MediaSendViewModel @Inject constructor(
if (filteredMedia.size > 0) {
val computedId: String = Stream.of(filteredMedia)
.skip(1)
.reduce(filteredMedia.get(0).bucketId.or(Media.ALL_MEDIA_BUCKET_ID),
.reduce(filteredMedia.get(0).bucketId ?: Media.ALL_MEDIA_BUCKET_ID,
{ id: String?, m: Media ->
if (equals(id, m.bucketId.or(Media.ALL_MEDIA_BUCKET_ID))) {
if (equals(id, m.bucketId ?: Media.ALL_MEDIA_BUCKET_ID)) {
return@reduce id
} else {
return@reduce Media.ALL_MEDIA_BUCKET_ID
@ -118,7 +118,7 @@ internal class MediaSendViewModel @Inject constructor(
error.setValue(Error.ITEM_TOO_LARGE)
bucketId.setValue(Media.ALL_MEDIA_BUCKET_ID)
} else {
bucketId.setValue(filteredMedia.get(0).bucketId.or(Media.ALL_MEDIA_BUCKET_ID))
bucketId.setValue(filteredMedia.get(0).bucketId ?: Media.ALL_MEDIA_BUCKET_ID)
}
countButtonVisibility = CountButtonState.Visibility.FORCED_OFF

@ -25,6 +25,7 @@ import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import kotlin.Pair;
@ -178,7 +179,7 @@ public class BlobProvider {
@WorkerThread
@NonNull
private static Future<Uri> writeBlobSpecToDisk(@NonNull Context context, @NonNull BlobSpec blobSpec, @Nullable ErrorListener errorListener) throws IOException {
private static CompletableFuture<Uri> writeBlobSpecToDisk(@NonNull Context context, @NonNull BlobSpec blobSpec, @Nullable ErrorListener errorListener) throws IOException {
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
String directory = getDirectory(blobSpec.getStorageType());
File outputFile = new File(getOrCreateCacheDirectory(context, directory), buildFileName(blobSpec.id));
@ -186,7 +187,7 @@ public class BlobProvider {
final Uri uri = buildUri(blobSpec);
return SignalExecutors.UNBOUNDED.submit(() -> {
return CompletableFuture.supplyAsync(() -> {
try {
Util.copy(blobSpec.getData(), outputStream);
return uri;
@ -195,9 +196,9 @@ public class BlobProvider {
errorListener.onError(e);
}
throw e;
throw new RuntimeException(e);
}
});
}, SignalExecutors.UNBOUNDED);
}
private synchronized @NonNull Uri writeBlobSpecToMemory(@NonNull BlobSpec blobSpec, @NonNull byte[] data) {
@ -266,7 +267,7 @@ public class BlobProvider {
* period from one {@link Application#onCreate()} to the next.
*/
@WorkerThread
public Future<Uri> createForSingleSessionOnDisk(@NonNull Context context, @Nullable ErrorListener errorListener) throws IOException {
public CompletableFuture<Uri> createForSingleSessionOnDisk(@NonNull Context context, @Nullable ErrorListener errorListener) throws IOException {
return writeBlobSpecToDisk(context, buildBlobSpec(StorageType.SINGLE_SESSION_DISK), errorListener);
}
@ -275,7 +276,7 @@ public class BlobProvider {
* eventually call {@link BlobProvider#delete(Context, Uri)} when the blob is no longer in use.
*/
@WorkerThread
public Future<Uri> createForMultipleSessionsOnDisk(@NonNull Context context, @Nullable ErrorListener errorListener) throws IOException {
public CompletableFuture<Uri> createForMultipleSessionsOnDisk(@NonNull Context context, @Nullable ErrorListener errorListener) throws IOException {
return writeBlobSpecToDisk(context, buildBlobSpec(StorageType.MULTI_SESSION_DISK), errorListener);
}
}

@ -118,7 +118,9 @@ public class BitmapUtil {
do {
totalAttempts++;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
scaledBitmap.compress(format, quality, baos);
if (!scaledBitmap.compress(format, quality, baos)) {
Log.d(TAG, "Unable to compress image with quality " + quality);
}
bytes = baos.toByteArray();
Log.d(TAG, "iteration with quality " + quality + " size " + bytes.length + " bytes.");
@ -144,7 +146,7 @@ public class BitmapUtil {
}
}
if (bytes.length <= 0) {
if (bytes.length == 0) {
throw new BitmapDecodingException("Decoding failed. Bitmap has a length of " + bytes.length + " bytes.");
}

Loading…
Cancel
Save