[SES-3368] - Convert MediaSendFragment to Kotlin (#1064)

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

@ -1,464 +0,0 @@
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;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.ImageButton;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.ViewPager;
import com.bumptech.glide.Glide;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import dagger.hilt.android.AndroidEntryPoint;
import network.loki.messenger.R;
import org.session.libsession.utilities.MediaTypes;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.Util;
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.imageeditor.model.EditorModel;
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment;
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
import org.thoughtcrime.securesms.util.PushCharacterCalculator;
import org.thoughtcrime.securesms.util.Stopwatch;
/**
* Allows the user to edit and caption a set of media items before choosing to send them.
*/
@AndroidEntryPoint
public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGlobalLayoutListener,
MediaRailAdapter.RailItemListener,
InputAwareLayout.OnKeyboardShownListener,
InputAwareLayout.OnKeyboardHiddenListener
{
private static final String TAG = MediaSendFragment.class.getSimpleName();
private static final String KEY_ADDRESS = "address";
private InputAwareLayout hud;
private View captionAndRail;
private ImageButton sendButton;
private ComposeText composeText;
private ViewGroup composeContainer;
private ViewGroup playbackControlsContainer;
private TextView charactersLeft;
private View closeButton;
private View loader;
private ControllableViewPager fragmentPager;
private MediaSendFragmentPagerAdapter fragmentPagerAdapter;
private RecyclerView mediaRail;
private MediaRailAdapter mediaRailAdapter;
private int visibleHeight;
private MediaSendViewModel viewModel;
private Controller controller;
private final Rect visibleBounds = new Rect();
private final PushCharacterCalculator characterCalculator = new PushCharacterCalculator();
public static MediaSendFragment newInstance(@NonNull Recipient recipient) {
Bundle args = new Bundle();
args.putParcelable(KEY_ADDRESS, recipient.getAddress());
MediaSendFragment fragment = new MediaSendFragment();
fragment.setArguments(args);
return fragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (!(requireActivity() instanceof Controller)) {
throw new IllegalStateException("Parent activity must implement controller interface.");
}
controller = (Controller) requireActivity();
viewModel = new ViewModelProvider(requireActivity()).get(MediaSendViewModel.class);
}
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.mediasend_fragment, container, false);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initViewModel();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
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);
View sendButtonBkg = view.findViewById(R.id.mediasend_send_button_bkg);
sendButton.setOnClickListener(v -> {
if (hud.isKeyboardOpen()) {
hud.hideSoftkey(composeText, null);
}
processMedia(fragmentPagerAdapter.getAllMedia(), fragmentPagerAdapter.getSavedState());
});
ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener();
composeText.setOnKeyListener(composeKeyPressedListener);
composeText.addTextChangedListener(composeKeyPressedListener);
composeText.setOnClickListener(composeKeyPressedListener);
composeText.setOnFocusChangeListener(composeKeyPressedListener);
composeText.requestFocus();
fragmentPagerAdapter = new MediaSendFragmentPagerAdapter(getChildFragmentManager());
fragmentPager.setAdapter(fragmentPagerAdapter);
FragmentPageChangeListener pageChangeListener = new FragmentPageChangeListener();
fragmentPager.addOnPageChangeListener(pageChangeListener);
fragmentPager.post(() -> pageChangeListener.onPageSelected(fragmentPager.getCurrentItem()));
mediaRailAdapter = new MediaRailAdapter(Glide.with(this), this, true);
mediaRail.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false));
mediaRail.setAdapter(mediaRailAdapter);
hud.getRootView().getViewTreeObserver().addOnGlobalLayoutListener(this);
hud.addOnKeyboardShownListener(this);
hud.addOnKeyboardHiddenListener(this);
composeText.append(viewModel.getBody());
Recipient recipient = Recipient.from(requireContext(), getArguments().getParcelable(KEY_ADDRESS), false);
String displayName = Optional.fromNullable(recipient.getName())
.or(Optional.fromNullable(recipient.getProfileName())
.or(recipient.getAddress().toString()));
composeText.setHint(getString(R.string.message), null);
composeText.setOnEditorActionListener((v, actionId, event) -> {
boolean isSend = actionId == EditorInfo.IME_ACTION_SEND;
if (isSend) sendButton.performClick();
return isSend;
});
closeButton.setOnClickListener(v -> requireActivity().onBackPressed());
}
@Override
public void onStart() {
super.onStart();
fragmentPagerAdapter.restoreState(viewModel.getDrawState());
viewModel.onImageEditorStarted();
requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
}
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
}
@Override
public void onStop() {
super.onStop();
fragmentPagerAdapter.saveAllState();
viewModel.saveDrawState(fragmentPagerAdapter.getSavedState());
}
@Override
public void onGlobalLayout() {
hud.getRootView().getWindowVisibleDisplayFrame(visibleBounds);
int currentVisibleHeight = visibleBounds.height();
if (currentVisibleHeight != visibleHeight) {
hud.getLayoutParams().height = currentVisibleHeight;
hud.layout(visibleBounds.left, visibleBounds.top, visibleBounds.right, visibleBounds.bottom);
hud.requestLayout();
visibleHeight = currentVisibleHeight;
}
}
@Override
public void onRailItemClicked(int distanceFromActive) {
viewModel.onPageChanged(fragmentPager.getCurrentItem() + distanceFromActive);
}
@Override
public void onRailItemDeleteClicked(int distanceFromActive) {
viewModel.onMediaItemRemoved(requireContext(), fragmentPager.getCurrentItem() + distanceFromActive);
}
@Override
public void onKeyboardShown() {
if (composeText.hasFocus()) {
mediaRail.setVisibility(View.VISIBLE);
composeContainer.setVisibility(View.VISIBLE);
} else {
mediaRail.setVisibility(View.GONE);
composeContainer.setVisibility(View.VISIBLE);
}
}
@Override
public void onKeyboardHidden() {
composeContainer.setVisibility(View.VISIBLE);
mediaRail.setVisibility(View.VISIBLE);
}
public void onTouchEventsNeeded(boolean needed) {
if (fragmentPager != null) {
fragmentPager.setEnabled(!needed);
}
}
public boolean handleBackPress() {
if (hud.isInputOpen()) {
hud.hideCurrentInput(composeText);
return true;
}
return false;
}
private void initViewModel() {
viewModel.getSelectedMedia().observe(this, media -> {
if (Util.isEmpty(media)) {
controller.onNoMediaAvailable();
return;
}
fragmentPagerAdapter.setMedia(media);
mediaRail.setVisibility(View.VISIBLE);
mediaRailAdapter.setMedia(media);
});
viewModel.getPosition().observe(this, position -> {
if (position == null || position < 0) return;
fragmentPager.setCurrentItem(position, true);
mediaRailAdapter.setActivePosition(position);
mediaRail.smoothScrollToPosition(position);
View playbackControls = fragmentPagerAdapter.getPlaybackControls(position);
if (playbackControls != null) {
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
playbackControls.setLayoutParams(params);
playbackControlsContainer.removeAllViews();
playbackControlsContainer.addView(playbackControls);
} else {
playbackControlsContainer.removeAllViews();
}
});
viewModel.getBucketId().observe(this, bucketId -> {
if (bucketId == null) return;
mediaRailAdapter.setAddButtonListener(() -> controller.onAddMediaClicked(bucketId));
});
}
private void presentCharactersRemaining() {
String messageBody = composeText.getTextTrimmed();
CharacterState characterState = characterCalculator.calculateCharacters(messageBody);
if (characterState.charactersRemaining <= 15 || characterState.messagesSpent > 1) {
charactersLeft.setText(String.format(Locale.getDefault(),
"%d/%d (%d)",
characterState.charactersRemaining,
characterState.maxTotalMessageSize,
characterState.messagesSpent));
charactersLeft.setVisibility(View.VISIBLE);
} else {
charactersLeft.setVisibility(View.GONE);
}
}
@SuppressLint("StaticFieldLeak")
private void processMedia(@NonNull List<Media> mediaList, @NonNull Map<Uri, Object> savedState) {
Map<Media, ListenableFuture<Bitmap>> futures = new HashMap<>();
for (Media media : mediaList) {
Object state = savedState.get(media.getUri());
if (state instanceof ImageEditorFragment.Data) {
EditorModel model = ((ImageEditorFragment.Data) state).readModel();
if (model != null && model.isChanged()) {
futures.put(media, render(requireContext(), model));
}
}
}
new AsyncTask<Void, Void, List<Media>>() {
private Stopwatch renderTimer;
private Runnable progressTimer;
@Override
protected void onPreExecute() {
renderTimer = new Stopwatch("ProcessMedia");
progressTimer = () -> {
loader.setVisibility(View.VISIBLE);
};
Util.runOnMainDelayed(progressTimer, 250);
}
@Override
protected List<Media> doInBackground(Void... voids) {
Context context = requireContext();
List<Media> updatedMedia = new ArrayList<>(mediaList.size());
for (Media media : mediaList) {
if (futures.containsKey(media)) {
try {
Bitmap bitmap = futures.get(media).get();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos);
Uri uri = BlobProvider.getInstance()
.forData(baos.toByteArray())
.withMimeType(MediaTypes.IMAGE_JPEG)
.createForSingleSessionOnDisk(context, e -> Log.w(TAG, "Failed to write to disk.", e));
Media updated = new Media(uri, media.getFilename(), MediaTypes.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), baos.size(), media.getBucketId(), media.getCaption());
updatedMedia.add(updated);
renderTimer.split("item");
} catch (InterruptedException | ExecutionException | IOException e) {
Log.w(TAG, "Failed to render image. Using base image.");
updatedMedia.add(media);
}
} else {
updatedMedia.add(media);
}
}
return updatedMedia;
}
@Override
protected void onPostExecute(List<Media> media) {
controller.onSendClicked(media, composeText.getTextTrimmed());
Util.cancelRunnableOnMain(progressTimer);
loader.setVisibility(View.GONE);
renderTimer.stop(TAG);
}
}.execute();
}
private static ListenableFuture<Bitmap> render(@NonNull Context context, @NonNull EditorModel model) {
SettableFuture<Bitmap> future = new SettableFuture<>();
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> future.set(model.render(context)));
return future;
}
public void onRequestFullScreen(boolean fullScreen) {
captionAndRail.setVisibility(fullScreen ? View.GONE : View.VISIBLE);
}
private class FragmentPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
@Override
public void onPageSelected(int position) {
viewModel.onPageChanged(position);
}
}
private class ComposeKeyPressedListener implements View.OnKeyListener, View.OnClickListener, TextWatcher, View.OnFocusChangeListener {
int beforeLength;
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (keyCode == KeyEvent.KEYCODE_ENTER) {
if (TextSecurePreferences.isEnterSendsEnabled(requireContext())) {
sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
return true;
}
}
}
return false;
}
@Override
public void onClick(View v) {
hud.showSoftkey(composeText);
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,int after) {
beforeLength = composeText.getTextTrimmed().length();
}
@Override
public void afterTextChanged(Editable s) {
presentCharactersRemaining();
viewModel.onBodyChanged(s);
}
@Override
public void onTextChanged(CharSequence s, int start, int before,int count) {}
@Override
public void onFocusChange(View v, boolean hasFocus) {}
}
public interface Controller {
void onAddMediaClicked(@NonNull String bucketId);
void onSendClicked(@NonNull List<Media> media, @NonNull String body);
void onNoMediaAvailable();
}
}

@ -0,0 +1,498 @@
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
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.View.OnFocusChangeListener
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.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
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 network.loki.messenger.R
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.util.Locale
import java.util.concurrent.ExecutionException
/**
* Allows the user to edit and caption a set of media items before choosing to send them.
*/
@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 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()
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
)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.mediasend_fragment, container, false)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initViewModel()
}
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)
}
processMedia(fragmentPagerAdapter!!.allMedia, fragmentPagerAdapter!!.savedState)
})
val composeKeyPressedListener = ComposeKeyPressedListener()
composeText!!.setOnKeyListener(composeKeyPressedListener)
composeText!!.addTextChangedListener(composeKeyPressedListener)
composeText!!.setOnClickListener(composeKeyPressedListener)
composeText!!.setOnFocusChangeListener(composeKeyPressedListener)
composeText!!.requestFocus()
fragmentPagerAdapter = MediaSendFragmentPagerAdapter(childFragmentManager)
fragmentPager!!.setAdapter(fragmentPagerAdapter)
val pageChangeListener = FragmentPageChangeListener()
fragmentPager!!.addOnPageChangeListener(pageChangeListener)
fragmentPager!!.post(Runnable { pageChangeListener.onPageSelected(fragmentPager!!.currentItem) })
mediaRailAdapter = MediaRailAdapter(Glide.with(this), this, true)
mediaRail!!.setLayoutManager(
LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
)
mediaRail!!.setAdapter(mediaRailAdapter)
hud!!.getRootView().viewTreeObserver.addOnGlobalLayoutListener(this)
hud!!.addOnKeyboardShownListener(this)
hud!!.addOnKeyboardHiddenListener(this)
composeText!!.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? ->
val isSend = actionId == EditorInfo.IME_ACTION_SEND
if (isSend) sendButton!!.performClick()
isSend
})
closeButton!!.setOnClickListener(View.OnClickListener { v: View? -> requireActivity().onBackPressed() })
}
override fun onStart() {
super.onStart()
fragmentPagerAdapter!!.restoreState(viewModel!!.drawState)
viewModel!!.onImageEditorStarted()
requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
}
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
}
override fun onStop() {
super.onStop()
fragmentPagerAdapter!!.saveAllState()
viewModel!!.saveDrawState(fragmentPagerAdapter!!.savedState)
}
override fun onGlobalLayout() {
hud!!.rootView.getWindowVisibleDisplayFrame(visibleBounds)
val currentVisibleHeight = visibleBounds.height()
if (currentVisibleHeight != visibleHeight) {
hud!!.layoutParams.height = currentVisibleHeight
hud!!.layout(
visibleBounds.left,
visibleBounds.top,
visibleBounds.right,
visibleBounds.bottom
)
hud!!.requestLayout()
visibleHeight = currentVisibleHeight
}
}
override fun onRailItemClicked(distanceFromActive: Int) {
viewModel!!.onPageChanged(fragmentPager!!.currentItem + distanceFromActive)
}
override fun onRailItemDeleteClicked(distanceFromActive: Int) {
viewModel!!.onMediaItemRemoved(
requireContext(),
fragmentPager!!.currentItem + distanceFromActive
)
}
override fun onKeyboardShown() {
if (composeText!!.hasFocus()) {
mediaRail!!.visibility = View.VISIBLE
composeContainer!!.visibility = View.VISIBLE
} else {
mediaRail!!.visibility = View.GONE
composeContainer!!.visibility = View.VISIBLE
}
}
override fun onKeyboardHidden() {
composeContainer!!.visibility = View.VISIBLE
mediaRail!!.visibility = View.VISIBLE
}
fun onTouchEventsNeeded(needed: Boolean) {
if (fragmentPager != null) {
fragmentPager!!.isEnabled = !needed
}
}
fun handleBackPress(): Boolean {
if (hud!!.isInputOpen) {
hud!!.hideCurrentInput(composeText)
return true
}
return false
}
private fun initViewModel() {
viewModel!!.getSelectedMedia().observe(
this
) { media: List<Media?>? ->
if (isEmpty(media)) {
controller!!.onNoMediaAvailable()
return@observe
}
fragmentPagerAdapter!!.setMedia(media!!)
mediaRail!!.visibility = View.VISIBLE
mediaRailAdapter!!.setMedia(media)
}
viewModel!!.getPosition().observe(this) { position: Int? ->
if (position == null || position < 0) return@observe
fragmentPager!!.setCurrentItem(position, true)
mediaRailAdapter!!.setActivePosition(position)
mediaRail!!.smoothScrollToPosition(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)
} else {
playbackControlsContainer!!.removeAllViews()
}
}
viewModel!!.getBucketId().observe(this) { bucketId: String? ->
if (bucketId == null) return@observe
mediaRailAdapter!!.setAddButtonListener { controller!!.onAddMediaClicked(bucketId) }
}
}
private fun presentCharactersRemaining() {
val messageBody = composeText!!.textTrimmed
val characterState = characterCalculator.calculateCharacters(messageBody)
if (characterState.charactersRemaining <= 15 || characterState.messagesSpent > 1) {
charactersLeft!!.text = String.format(
Locale.getDefault(),
"%d/%d (%d)",
characterState.charactersRemaining,
characterState.maxTotalMessageSize,
characterState.messagesSpent
)
charactersLeft!!.visibility = View.VISIBLE
} else {
charactersLeft!!.visibility = View.GONE
}
}
@SuppressLint("StaticFieldLeak")
private fun processMedia(mediaList: List<Media>, savedState: Map<Uri, Any>) {
val futures: MutableMap<Media, ListenableFuture<Bitmap>> = HashMap()
for (media in mediaList) {
val state = savedState[media.uri]
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)
}
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 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: InterruptedException) {
Log.w(TAG, "Failed to render image. Using base image.")
updatedMedia.add(media)
} catch (e: ExecutionException) {
Log.w(TAG, "Failed to render image. Using base image.")
updatedMedia.add(media)
} catch (e: IOException) {
Log.w(TAG, "Failed to render image. Using base image.")
updatedMedia.add(media)
}
} else {
updatedMedia.add(media)
}
}
return updatedMedia
}
override fun onPostExecute(media: List<Media>) {
controller!!.onSendClicked(media, composeText!!.textTrimmed)
cancelRunnableOnMain(progressTimer!!)
loader!!.visibility = View.GONE
renderTimer!!.stop(TAG)
}
}.execute()
}
fun onRequestFullScreen(fullScreen: Boolean) {
captionAndRail!!.visibility =
if (fullScreen) View.GONE else View.VISIBLE
}
private inner class FragmentPageChangeListener : SimpleOnPageChangeListener() {
override fun onPageSelected(position: Int) {
viewModel!!.onPageChanged(position)
}
}
private inner class ComposeKeyPressedListener : View.OnKeyListener, View.OnClickListener,
TextWatcher, OnFocusChangeListener {
var beforeLength: Int = 0
override fun onKey(v: View, keyCode: Int, event: KeyEvent): Boolean {
if (event.action == KeyEvent.ACTION_DOWN) {
if (keyCode == KeyEvent.KEYCODE_ENTER) {
if (isEnterSendsEnabled(requireContext())) {
sendButton!!.dispatchKeyEvent(
KeyEvent(
KeyEvent.ACTION_DOWN,
KeyEvent.KEYCODE_ENTER
)
)
sendButton!!.dispatchKeyEvent(
KeyEvent(
KeyEvent.ACTION_UP,
KeyEvent.KEYCODE_ENTER
)
)
return true
}
}
}
return false
}
override fun onClick(v: View) {
hud!!.showSoftkey(composeText)
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
beforeLength = composeText!!.textTrimmed.length
}
override fun afterTextChanged(s: Editable) {
presentCharactersRemaining()
viewModel!!.onBodyChanged(s)
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
override fun onFocusChange(v: View, hasFocus: Boolean) {}
}
interface Controller {
fun onAddMediaClicked(bucketId: String)
fun onSendClicked(media: List<Media>, body: String)
fun onNoMediaAvailable()
}
companion object {
private val TAG: String = MediaSendFragment::class.java.simpleName
private const val KEY_ADDRESS = "address"
fun newInstance(recipient: Recipient): MediaSendFragment {
val args = Bundle()
args.putParcelable(KEY_ADDRESS, recipient.address)
val fragment = MediaSendFragment()
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
}
}
}
Loading…
Cancel
Save