Konverting the MediaSendViewModel (#922)

* Konverting the MediaSendViewModel

* PR Feedback
pull/1710/head
ThomasSession 3 months ago committed by GitHub
parent b134b82daa
commit 14948df4b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -34,6 +34,7 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.request.target.SimpleTarget; import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.request.transition.Transition;
import dagger.hilt.android.AndroidEntryPoint;
import network.loki.messenger.R; import network.loki.messenger.R;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
@ -43,6 +44,7 @@ import org.session.libsession.utilities.TextSecurePreferences;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@AndroidEntryPoint
public class Camera1Fragment extends Fragment implements TextureView.SurfaceTextureListener, public class Camera1Fragment extends Fragment implements TextureView.SurfaceTextureListener,
Camera1Controller.EventListener Camera1Controller.EventListener
{ {
@ -80,7 +82,7 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
controller = (Controller) getActivity(); controller = (Controller) getActivity();
camera = new Camera1Controller(TextSecurePreferences.getDirectCaptureCameraId(getContext()), displaySize.x, displaySize.y, this); camera = new Camera1Controller(TextSecurePreferences.getDirectCaptureCameraId(getContext()), displaySize.x, displaySize.y, this);
orderEnforcer = new OrderEnforcer<>(Stage.SURFACE_AVAILABLE, Stage.CAMERA_PROPERTIES_AVAILABLE); orderEnforcer = new OrderEnforcer<>(Stage.SURFACE_AVAILABLE, Stage.CAMERA_PROPERTIES_AVAILABLE);
viewModel = new ViewModelProvider(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class); viewModel = new ViewModelProvider(requireActivity()).get(MediaSendViewModel.class);
} }
@Nullable @Nullable

@ -28,11 +28,13 @@ import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.guava.Optional; import org.session.libsignal.utilities.guava.Optional;
import dagger.hilt.android.AndroidEntryPoint;
import network.loki.messenger.R; import network.loki.messenger.R;
/** /**
* Allows the user to select a media folder to explore. * Allows the user to select a media folder to explore.
*/ */
@AndroidEntryPoint
public class MediaPickerFolderFragment extends Fragment implements MediaPickerFolderAdapter.EventListener { public class MediaPickerFolderFragment extends Fragment implements MediaPickerFolderAdapter.EventListener {
private static final String KEY_RECIPIENT_NAME = "recipient_name"; private static final String KEY_RECIPIENT_NAME = "recipient_name";
@ -60,7 +62,7 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
recipientName = getArguments().getString(KEY_RECIPIENT_NAME); recipientName = getArguments().getString(KEY_RECIPIENT_NAME);
viewModel = new ViewModelProvider(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class); viewModel = new ViewModelProvider(requireActivity()).get(MediaSendViewModel.class);
} }
@Override @Override

@ -27,11 +27,13 @@ import org.session.libsession.utilities.Util;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import dagger.hilt.android.AndroidEntryPoint;
import network.loki.messenger.R; import network.loki.messenger.R;
/** /**
* Allows the user to select a set of media items from a specified folder. * Allows the user to select a set of media items from a specified folder.
*/ */
@AndroidEntryPoint
public class MediaPickerItemFragment extends Fragment implements MediaPickerItemAdapter.EventListener { public class MediaPickerItemFragment extends Fragment implements MediaPickerItemAdapter.EventListener {
private static final String KEY_BUCKET_ID = "bucket_id"; private static final String KEY_BUCKET_ID = "bucket_id";
@ -66,7 +68,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
bucketId = getArguments().getString(KEY_BUCKET_ID); bucketId = getArguments().getString(KEY_BUCKET_ID);
folderTitle = getArguments().getString(KEY_FOLDER_TITLE); folderTitle = getArguments().getString(KEY_FOLDER_TITLE);
maxSelection = getArguments().getInt(KEY_MAX_SELECTION); maxSelection = getArguments().getInt(KEY_MAX_SELECTION);
viewModel = new ViewModelProvider(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class); viewModel = new ViewModelProvider(requireActivity()).get(MediaSendViewModel.class);
} }
@Override @Override

@ -13,10 +13,11 @@ import android.view.animation.OvershootInterpolator
import android.view.animation.ScaleAnimation import android.view.animation.ScaleAnimation
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.viewModels
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.ViewModelProvider
import com.squareup.phrase.Phrase import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.MediaTypes import org.session.libsession.utilities.MediaTypes
@ -41,12 +42,13 @@ import java.io.IOException
* This activity is intended to be launched via [.startActivityForResult]. * This activity is intended to be launched via [.startActivityForResult].
* It will return the [Media] that the user decided to send. * It will return the [Media] that the user decided to send.
*/ */
@AndroidEntryPoint
class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragment.Controller, class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragment.Controller,
MediaPickerItemFragment.Controller, MediaSendFragment.Controller, MediaPickerItemFragment.Controller, MediaSendFragment.Controller,
ImageEditorFragment.Controller, ImageEditorFragment.Controller,
Camera1Fragment.Controller { Camera1Fragment.Controller {
private var recipient: Recipient? = null private var recipient: Recipient? = null
private var viewModel: MediaSendViewModel? = null private val viewModel: MediaSendViewModel by viewModels()
private var countButton: View? = null private var countButton: View? = null
private var countButtonText: TextView? = null private var countButtonText: TextView? = null
@ -66,20 +68,13 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme
countButtonText = findViewById(R.id.mediasend_count_button_text) countButtonText = findViewById(R.id.mediasend_count_button_text)
cameraButton = findViewById(R.id.mediasend_camera_button) cameraButton = findViewById(R.id.mediasend_camera_button)
viewModel = ViewModelProvider(
this, MediaSendViewModel.Factory(
application, MediaRepository()
)
).get(
MediaSendViewModel::class.java
)
recipient = Recipient.from( recipient = Recipient.from(
this, fromSerialized( this, fromSerialized(
intent.getStringExtra(KEY_ADDRESS)!! intent.getStringExtra(KEY_ADDRESS)!!
), true ), true
) )
viewModel!!.onBodyChanged(intent.getStringExtra(KEY_BODY)!!) viewModel.onBodyChanged(intent.getStringExtra(KEY_BODY)!!)
val media: List<Media?>? = intent.getParcelableArrayListExtra(KEY_MEDIA) val media: List<Media?>? = intent.getParcelableArrayListExtra(KEY_MEDIA)
val isCamera = intent.getBooleanExtra(KEY_IS_CAMERA, false) val isCamera = intent.getBooleanExtra(KEY_IS_CAMERA, false)
@ -90,7 +85,7 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme
.replace(R.id.mediasend_fragment_container, fragment, TAG_CAMERA) .replace(R.id.mediasend_fragment_container, fragment, TAG_CAMERA)
.commit() .commit()
} else if (!isEmpty(media)) { } else if (!isEmpty(media)) {
viewModel!!.onSelectedMediaChanged(this, media!!) viewModel.onSelectedMediaChanged(this, media!!)
val fragment: Fragment = MediaSendFragment.newInstance(recipient!!) val fragment: Fragment = MediaSendFragment.newInstance(recipient!!)
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
@ -110,8 +105,8 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme
initializeErrorObserver() initializeErrorObserver()
cameraButton?.setOnClickListener { v: View? -> cameraButton?.setOnClickListener { v: View? ->
val maxSelection = viewModel!!.maxSelection val maxSelection = MediaSendViewModel.MAX_SELECTED_FILES
if (viewModel!!.selectedMedia.value != null && viewModel!!.selectedMedia.value!!.size >= maxSelection) { if (viewModel.getSelectedMedia().value != null && viewModel.getSelectedMedia().value!!.size >= maxSelection) {
Toast.makeText(this, getString(R.string.attachmentsErrorNumber), Toast.LENGTH_SHORT) Toast.makeText(this, getString(R.string.attachmentsErrorNumber), Toast.LENGTH_SHORT)
.show() .show()
} else { } else {
@ -130,7 +125,7 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme
false false
) && supportFragmentManager.backStackEntryCount == 0 ) && supportFragmentManager.backStackEntryCount == 0
) { ) {
viewModel!!.onImageCaptureUndo(this) viewModel.onImageCaptureUndo(this)
} }
} }
} }
@ -145,16 +140,12 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme
} }
override fun onFolderSelected(folder: MediaFolder) { override fun onFolderSelected(folder: MediaFolder) {
if (viewModel == null) { viewModel.onFolderSelected(folder.bucketId)
return
}
viewModel!!.onFolderSelected(folder.bucketId)
val fragment = MediaPickerItemFragment.newInstance( val fragment = MediaPickerItemFragment.newInstance(
folder.bucketId, folder.bucketId,
folder.title, folder.title,
viewModel!!.maxSelection MediaSendViewModel.MAX_SELECTED_FILES
) )
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.setCustomAnimations( .setCustomAnimations(
@ -169,7 +160,7 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme
} }
override fun onMediaSelected(media: Media) { override fun onMediaSelected(media: Media) {
viewModel!!.onSingleMediaSelected(this, media) viewModel.onSingleMediaSelected(this, media)
navigateToMediaSend(recipient!!) navigateToMediaSend(recipient!!)
} }
@ -178,7 +169,7 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme
recipient!! recipient!!
) )
val itemFragment = val itemFragment =
MediaPickerItemFragment.newInstance(bucketId, "", viewModel!!.maxSelection) MediaPickerItemFragment.newInstance(bucketId, "", MediaSendViewModel.MAX_SELECTED_FILES)
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.setCustomAnimations( .setCustomAnimations(
@ -204,7 +195,7 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme
} }
override fun onSendClicked(media: List<Media>, message: String) { override fun onSendClicked(media: List<Media>, message: String) {
viewModel!!.onSendClicked() viewModel.onSendClicked()
val mediaList = ArrayList(media) val mediaList = ArrayList(media)
val intent = Intent() val intent = Intent()
@ -271,7 +262,7 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme
} }
Log.i(TAG, "Camera capture stored: " + media.uri.toString()) Log.i(TAG, "Camera capture stored: " + media.uri.toString())
viewModel!!.onImageCaptured(media) viewModel.onImageCaptured(media)
navigateToMediaSend(recipient!!) navigateToMediaSend(recipient!!)
}) })
} }
@ -281,7 +272,7 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme
} }
private fun initializeCountButtonObserver() { private fun initializeCountButtonObserver() {
viewModel!!.countButtonState.observe( viewModel.getCountButtonState().observe(
this this
) { buttonState: CountButtonState? -> ) { buttonState: CountButtonState? ->
if (buttonState == null) return@observe if (buttonState == null) return@observe
@ -308,7 +299,7 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme
} }
private fun initializeCameraButtonObserver() { private fun initializeCameraButtonObserver() {
viewModel!!.cameraButtonVisibility.observe( viewModel.getCameraButtonVisibility().observe(
this this
) { visible: Boolean? -> ) { visible: Boolean? ->
if (visible == null) return@observe if (visible == null) return@observe
@ -321,7 +312,7 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme
} }
private fun initializeErrorObserver() { private fun initializeErrorObserver() {
viewModel!!.error.observe( viewModel.getError().observe(
this this
) { error: MediaSendViewModel.Error? -> ) { error: MediaSendViewModel.Error? ->
if (error == null) return@observe if (error == null) return@observe

@ -36,6 +36,8 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import dagger.hilt.android.AndroidEntryPoint;
import network.loki.messenger.R; import network.loki.messenger.R;
import org.session.libsession.utilities.MediaTypes; import org.session.libsession.utilities.MediaTypes;
import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.TextSecurePreferences;
@ -59,6 +61,7 @@ import org.thoughtcrime.securesms.util.Stopwatch;
/** /**
* Allows the user to edit and caption a set of media items before choosing to send them. * 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, public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGlobalLayoutListener,
MediaRailAdapter.RailItemListener, MediaRailAdapter.RailItemListener,
InputAwareLayout.OnKeyboardShownListener, InputAwareLayout.OnKeyboardShownListener,
@ -108,6 +111,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
} }
controller = (Controller) requireActivity(); controller = (Controller) requireActivity();
viewModel = new ViewModelProvider(requireActivity()).get(MediaSendViewModel.class);
} }
@Override @Override
@ -264,8 +268,6 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
} }
private void initViewModel() { private void initViewModel() {
viewModel = new ViewModelProvider(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
viewModel.getSelectedMedia().observe(this, media -> { viewModel.getSelectedMedia().observe(this, media -> {
if (Util.isEmpty(media)) { if (Util.isEmpty(media)) {
controller.onNoMediaAvailable(); controller.onNoMediaAvailable();

@ -1,360 +0,0 @@
package org.thoughtcrime.securesms.mediasend;
import android.app.Application;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import com.annimon.stream.Stream;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.session.libsession.utilities.FileUtils;
import org.session.libsession.utilities.Util;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.guava.Optional;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
/**
* Manages the observable datasets available in {@link MediaSendActivity}.
*/
class MediaSendViewModel extends ViewModel {
private static final String TAG = MediaSendViewModel.class.getSimpleName();
private static final int MAX_SELECTION = 32;
private final Application application;
private final MediaRepository repository;
private final MutableLiveData<List<Media>> selectedMedia;
private final MutableLiveData<List<Media>> bucketMedia;
private final MutableLiveData<Integer> position;
private final MutableLiveData<String> bucketId;
private final MutableLiveData<List<MediaFolder>> folders;
private final MutableLiveData<CountButtonState> countButtonState;
private final MutableLiveData<Boolean> cameraButtonVisibility;
private final SingleLiveEvent<Error> error;
private final Map<Uri, Object> savedDrawState;
private final MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints();
private CharSequence body;
private CountButtonState.Visibility countButtonVisibility;
private boolean sentMedia;
private Optional<Media> lastImageCapture;
private MediaSendViewModel(@NonNull Application application, @NonNull MediaRepository repository) {
this.application = application;
this.repository = repository;
this.selectedMedia = new MutableLiveData<>();
this.bucketMedia = new MutableLiveData<>();
this.position = new MutableLiveData<>();
this.bucketId = new MutableLiveData<>();
this.folders = new MutableLiveData<>();
this.countButtonState = new MutableLiveData<>();
this.cameraButtonVisibility = new MutableLiveData<>();
this.error = new SingleLiveEvent<>();
this.savedDrawState = new HashMap<>();
this.countButtonVisibility = CountButtonState.Visibility.FORCED_OFF;
this.lastImageCapture = Optional.absent();
this.body = "";
position.setValue(-1);
countButtonState.setValue(new CountButtonState(0, countButtonVisibility));
cameraButtonVisibility.setValue(false);
}
void onSelectedMediaChanged(@NonNull Context context, @NonNull List<Media> newMedia) {
repository.getPopulatedMedia(context, newMedia, populatedMedia -> {
Util.runOnMain(() -> {
List<Media> filteredMedia = getFilteredMedia(context, populatedMedia, mediaConstraints);
if (filteredMedia.size() != newMedia.size()) {
error.setValue(Error.ITEM_TOO_LARGE);
} else if (filteredMedia.size() > MAX_SELECTION) {
filteredMedia = filteredMedia.subList(0, MAX_SELECTION);
error.setValue(Error.TOO_MANY_ITEMS);
}
if (filteredMedia.size() > 0) {
String computedId = Stream.of(filteredMedia)
.skip(1)
.reduce(filteredMedia.get(0).getBucketId().or(Media.ALL_MEDIA_BUCKET_ID), (id, m) -> {
if (Util.equals(id, m.getBucketId().or(Media.ALL_MEDIA_BUCKET_ID))) {
return id;
} else {
return Media.ALL_MEDIA_BUCKET_ID;
}
});
bucketId.setValue(computedId);
} else {
bucketId.setValue(Media.ALL_MEDIA_BUCKET_ID);
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
}
selectedMedia.setValue(filteredMedia);
countButtonState.setValue(new CountButtonState(filteredMedia.size(), countButtonVisibility));
});
});
}
void onSingleMediaSelected(@NonNull Context context, @NonNull Media media) {
repository.getPopulatedMedia(context, Collections.singletonList(media), populatedMedia -> {
Util.runOnMain(() -> {
List<Media> filteredMedia = getFilteredMedia(context, populatedMedia, mediaConstraints);
if (filteredMedia.isEmpty()) {
error.setValue(Error.ITEM_TOO_LARGE);
bucketId.setValue(Media.ALL_MEDIA_BUCKET_ID);
} else {
bucketId.setValue(filteredMedia.get(0).getBucketId().or(Media.ALL_MEDIA_BUCKET_ID));
}
countButtonVisibility = CountButtonState.Visibility.FORCED_OFF;
selectedMedia.setValue(filteredMedia);
countButtonState.setValue(new CountButtonState(filteredMedia.size(), countButtonVisibility));
});
});
}
void onMultiSelectStarted() {
countButtonVisibility = CountButtonState.Visibility.FORCED_ON;
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
}
void onImageEditorStarted() {
countButtonVisibility = CountButtonState.Visibility.FORCED_OFF;
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
cameraButtonVisibility.setValue(false);
}
void onCameraStarted() {
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
cameraButtonVisibility.setValue(false);
}
void onItemPickerStarted() {
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
cameraButtonVisibility.setValue(true);
}
void onFolderPickerStarted() {
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
cameraButtonVisibility.setValue(true);
}
void onBodyChanged(@NonNull CharSequence body) {
this.body = body;
}
void onFolderSelected(@NonNull String bucketId) {
this.bucketId.setValue(bucketId);
bucketMedia.setValue(Collections.emptyList());
}
void onPageChanged(int position) {
if (position < 0 || position >= getSelectedMediaOrDefault().size()) {
Log.w(TAG, "Tried to move to an out-of-bounds item. Size: " + getSelectedMediaOrDefault().size() + ", position: " + position);
return;
}
this.position.setValue(position);
}
void onMediaItemRemoved(@NonNull Context context, int position) {
if (position < 0 || position >= getSelectedMediaOrDefault().size()) {
Log.w(TAG, "Tried to remove an out-of-bounds item. Size: " + getSelectedMediaOrDefault().size() + ", position: " + position);
return;
}
Media removed = getSelectedMediaOrDefault().remove(position);
if (removed != null && BlobProvider.isAuthority(removed.getUri())) {
BlobProvider.getInstance().delete(context, removed.getUri());
}
selectedMedia.setValue(selectedMedia.getValue());
}
void onImageCaptured(@NonNull Media media) {
List<Media> selected = selectedMedia.getValue();
if (selected == null) {
selected = new LinkedList<>();
}
if (selected.size() >= MAX_SELECTION) {
error.setValue(Error.TOO_MANY_ITEMS);
return;
}
lastImageCapture = Optional.of(media);
selected.add(media);
selectedMedia.setValue(selected);
position.setValue(selected.size() - 1);
bucketId.setValue(Media.ALL_MEDIA_BUCKET_ID);
if (selected.size() == 1) {
countButtonVisibility = CountButtonState.Visibility.FORCED_OFF;
} else {
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
}
countButtonState.setValue(new CountButtonState(selected.size(), countButtonVisibility));
}
void onImageCaptureUndo(@NonNull Context context) {
List<Media> selected = getSelectedMediaOrDefault();
if (lastImageCapture.isPresent() && selected.contains(lastImageCapture.get()) && selected.size() == 1) {
selected.remove(lastImageCapture.get());
selectedMedia.setValue(selected);
countButtonState.setValue(new CountButtonState(selected.size(), countButtonVisibility));
BlobProvider.getInstance().delete(context, lastImageCapture.get().getUri());
}
}
void saveDrawState(@NonNull Map<Uri, Object> state) {
savedDrawState.clear();
savedDrawState.putAll(state);
}
void onSendClicked() {
sentMedia = true;
}
@NonNull Map<Uri, Object> getDrawState() {
return savedDrawState;
}
@NonNull LiveData<List<Media>> getSelectedMedia() {
return selectedMedia;
}
@NonNull LiveData<List<Media>> getMediaInBucket(@NonNull Context context, @NonNull String bucketId) {
repository.getMediaInBucket(context, bucketId, bucketMedia::postValue);
return bucketMedia;
}
@NonNull LiveData<List<MediaFolder>> getFolders(@NonNull Context context) {
repository.getFolders(context, folders::postValue);
return folders;
}
@NonNull LiveData<CountButtonState> getCountButtonState() {
return countButtonState;
}
@NonNull LiveData<Boolean> getCameraButtonVisibility() {
return cameraButtonVisibility;
}
@NonNull CharSequence getBody() {
return body;
}
@NonNull LiveData<Integer> getPosition() {
return position;
}
@NonNull LiveData<String> getBucketId() {
return bucketId;
}
@NonNull LiveData<Error> getError() {
return error;
}
int getMaxSelection() {
return MAX_SELECTION;
}
private @NonNull List<Media> getSelectedMediaOrDefault() {
return selectedMedia.getValue() == null ? Collections.emptyList()
: selectedMedia.getValue();
}
private @NonNull List<Media> getFilteredMedia(@NonNull Context context, @NonNull List<Media> media, @NonNull MediaConstraints mediaConstraints) {
return Stream.of(media).filter(m -> MediaUtil.isGif(m.getMimeType()) ||
MediaUtil.isImageType(m.getMimeType()) ||
MediaUtil.isVideoType(m.getMimeType()))
.filter(m -> {
return (MediaUtil.isImageType(m.getMimeType()) && !MediaUtil.isGif(m.getMimeType())) ||
(MediaUtil.isGif(m.getMimeType()) && m.getSize() < mediaConstraints.getGifMaxSize(context)) ||
(MediaUtil.isVideoType(m.getMimeType()) && m.getSize() < mediaConstraints.getVideoMaxSize(context));
}).toList();
}
@Override
protected void onCleared() {
if (!sentMedia) {
Stream.of(getSelectedMediaOrDefault())
.map(Media::getUri)
.filter(BlobProvider::isAuthority)
.forEach(uri -> BlobProvider.getInstance().delete(application.getApplicationContext(), uri));
}
}
enum Error {
ITEM_TOO_LARGE, TOO_MANY_ITEMS
}
static class CountButtonState {
private final int count;
private final Visibility visibility;
private CountButtonState(int count, @NonNull Visibility visibility) {
this.count = count;
this.visibility = visibility;
}
int getCount() {
return count;
}
boolean isVisible() {
switch (visibility) {
case FORCED_ON: return true;
case FORCED_OFF: return false;
case CONDITIONAL: return count > 0;
default: return false;
}
}
enum Visibility {
CONDITIONAL, FORCED_ON, FORCED_OFF
}
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
private final Application application;
private final MediaRepository repository;
Factory(@NonNull Application application, @NonNull MediaRepository repository) {
this.application = application;
this.repository = repository;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return modelClass.cast(new MediaSendViewModel(application, repository));
}
}
}

@ -0,0 +1,367 @@
package org.thoughtcrime.securesms.mediasend
import android.app.Application
import android.content.Context
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.annimon.stream.Stream
import dagger.hilt.android.lifecycle.HiltViewModel
import org.session.libsession.utilities.Util.equals
import org.session.libsession.utilities.Util.runOnMain
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.SingleLiveEvent
import java.util.LinkedList
import javax.inject.Inject
/**
* Manages the observable datasets available in [MediaSendActivity].
*/
@HiltViewModel
internal class MediaSendViewModel @Inject constructor(
private val application: Application
) : ViewModel() {
private val selectedMedia: MutableLiveData<List<Media>?>
private val bucketMedia: MutableLiveData<List<Media>>
private val position: MutableLiveData<Int>
private val bucketId: MutableLiveData<String>
private val folders: MutableLiveData<List<MediaFolder>>
private val countButtonState: MutableLiveData<CountButtonState>
private val cameraButtonVisibility: MutableLiveData<Boolean>
private val error: SingleLiveEvent<Error>
private val savedDrawState: MutableMap<Uri, Any>
private val mediaConstraints: MediaConstraints = MediaConstraints.getPushMediaConstraints()
private val repository: MediaRepository = MediaRepository()
var body: CharSequence
private set
private var countButtonVisibility: CountButtonState.Visibility
private var sentMedia: Boolean = false
private var lastImageCapture: Optional<Media>
init {
this.selectedMedia = MutableLiveData()
this.bucketMedia = MutableLiveData()
this.position = MutableLiveData()
this.bucketId = MutableLiveData()
this.folders = MutableLiveData()
this.countButtonState = MutableLiveData()
this.cameraButtonVisibility = MutableLiveData()
this.error = SingleLiveEvent()
this.savedDrawState = HashMap()
this.countButtonVisibility = CountButtonState.Visibility.FORCED_OFF
this.lastImageCapture = Optional.absent()
this.body = ""
position.value = -1
countButtonState.value = CountButtonState(0, countButtonVisibility)
cameraButtonVisibility.value = false
}
fun onSelectedMediaChanged(context: Context, newMedia: List<Media?>) {
repository.getPopulatedMedia(context, newMedia,
{ populatedMedia: List<Media> ->
runOnMain(
{
var filteredMedia: List<Media> =
getFilteredMedia(context, populatedMedia, mediaConstraints)
if (filteredMedia.size != newMedia.size) {
error.setValue(Error.ITEM_TOO_LARGE)
} else if (filteredMedia.size > MAX_SELECTED_FILES) {
filteredMedia = filteredMedia.subList(0, MAX_SELECTED_FILES)
error.setValue(Error.TOO_MANY_ITEMS)
}
if (filteredMedia.size > 0) {
val computedId: String = Stream.of(filteredMedia)
.skip(1)
.reduce(filteredMedia.get(0).bucketId.or(Media.ALL_MEDIA_BUCKET_ID),
{ id: String?, m: Media ->
if (equals(id, m.bucketId.or(Media.ALL_MEDIA_BUCKET_ID))) {
return@reduce id
} else {
return@reduce Media.ALL_MEDIA_BUCKET_ID
}
})
bucketId.setValue(computedId)
} else {
bucketId.setValue(Media.ALL_MEDIA_BUCKET_ID)
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL
}
selectedMedia.setValue(filteredMedia)
countButtonState.setValue(
CountButtonState(
filteredMedia.size,
countButtonVisibility
)
)
})
})
}
fun onSingleMediaSelected(context: Context, media: Media) {
repository.getPopulatedMedia(context, listOf(media),
{ populatedMedia: List<Media> ->
runOnMain(
{
val filteredMedia: List<Media> =
getFilteredMedia(context, populatedMedia, mediaConstraints)
if (filteredMedia.isEmpty()) {
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))
}
countButtonVisibility = CountButtonState.Visibility.FORCED_OFF
selectedMedia.value = filteredMedia
countButtonState.setValue(
CountButtonState(
filteredMedia.size,
countButtonVisibility
)
)
})
})
}
fun onMultiSelectStarted() {
countButtonVisibility = CountButtonState.Visibility.FORCED_ON
countButtonState.value =
CountButtonState(selectedMediaOrDefault.size, countButtonVisibility)
}
fun onImageEditorStarted() {
countButtonVisibility = CountButtonState.Visibility.FORCED_OFF
countButtonState.value =
CountButtonState(selectedMediaOrDefault.size, countButtonVisibility)
cameraButtonVisibility.value = false
}
fun onCameraStarted() {
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL
countButtonState.value =
CountButtonState(selectedMediaOrDefault.size, countButtonVisibility)
cameraButtonVisibility.value = false
}
fun onItemPickerStarted() {
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL
countButtonState.value =
CountButtonState(selectedMediaOrDefault.size, countButtonVisibility)
cameraButtonVisibility.value = true
}
fun onFolderPickerStarted() {
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL
countButtonState.value =
CountButtonState(selectedMediaOrDefault.size, countButtonVisibility)
cameraButtonVisibility.value = true
}
fun onBodyChanged(body: CharSequence) {
this.body = body
}
fun onFolderSelected(bucketId: String) {
this.bucketId.value = bucketId
bucketMedia.value =
emptyList()
}
fun onPageChanged(position: Int) {
if (position < 0 || position >= selectedMediaOrDefault.size) {
Log.w(TAG,
"Tried to move to an out-of-bounds item. Size: " + selectedMediaOrDefault.size + ", position: " + position
)
return
}
this.position.value = position
}
fun onMediaItemRemoved(context: Context, position: Int) {
if (position < 0 || position >= selectedMediaOrDefault.size) {
Log.w(
TAG,
"Tried to remove an out-of-bounds item. Size: " + selectedMediaOrDefault.size + ", position: " + position
)
return
}
val updatedList = selectedMediaOrDefault.toMutableList()
val removed: Media = updatedList.removeAt(position)
if (BlobProvider.isAuthority(removed.uri)) {
BlobProvider.getInstance().delete(context, removed.uri)
}
selectedMedia.setValue(updatedList)
}
fun onImageCaptured(media: Media) {
var selected: MutableList<Media>? = selectedMedia.value?.toMutableList()
if (selected == null) {
selected = LinkedList()
}
if (selected.size >= MAX_SELECTED_FILES) {
error.setValue(Error.TOO_MANY_ITEMS)
return
}
lastImageCapture = Optional.of(media)
selected.add(media)
selectedMedia.setValue(selected)
position.setValue(selected.size - 1)
bucketId.setValue(Media.ALL_MEDIA_BUCKET_ID)
if (selected.size == 1) {
countButtonVisibility = CountButtonState.Visibility.FORCED_OFF
} else {
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL
}
countButtonState.setValue(CountButtonState(selected.size, countButtonVisibility))
}
fun onImageCaptureUndo(context: Context) {
val selected: MutableList<Media> = selectedMediaOrDefault.toMutableList()
if (lastImageCapture.isPresent && selected.contains(lastImageCapture.get()) && selected.size == 1) {
selected.remove(lastImageCapture.get())
selectedMedia.value = selected
countButtonState.value = CountButtonState(selected.size, countButtonVisibility)
BlobProvider.getInstance().delete(context, lastImageCapture.get().uri)
}
}
fun saveDrawState(state: Map<Uri, Any>) {
savedDrawState.clear()
savedDrawState.putAll(state)
}
fun onSendClicked() {
sentMedia = true
}
val drawState: Map<Uri, Any>
get() = savedDrawState
fun getSelectedMedia(): LiveData<List<Media>?> {
return selectedMedia
}
fun getMediaInBucket(context: Context, bucketId: String): LiveData<List<Media>> {
repository.getMediaInBucket(context, bucketId,
{ value: List<Media> -> bucketMedia.postValue(value) })
return bucketMedia
}
fun getFolders(context: Context): LiveData<List<MediaFolder>> {
repository.getFolders(context,
{ value: List<MediaFolder> -> folders.postValue(value) })
return folders
}
fun getCountButtonState(): LiveData<CountButtonState> {
return countButtonState
}
fun getCameraButtonVisibility(): LiveData<Boolean> {
return cameraButtonVisibility
}
fun getPosition(): LiveData<Int> {
return position
}
fun getBucketId(): LiveData<String> {
return bucketId
}
fun getError(): LiveData<Error> {
return error
}
private val selectedMediaOrDefault: List<Media>
get() = if (selectedMedia.value == null) emptyList() else
selectedMedia.value!!
private fun getFilteredMedia(
context: Context,
media: List<Media>,
mediaConstraints: MediaConstraints
): List<Media> {
return Stream.of(media).filter(
{ m: Media ->
MediaUtil.isGif(m.mimeType) ||
MediaUtil.isImageType(m.mimeType) ||
MediaUtil.isVideoType(m.mimeType)
})
.filter({ m: Media ->
(MediaUtil.isImageType(m.mimeType) && !MediaUtil.isGif(m.mimeType)) ||
(MediaUtil.isGif(m.mimeType) && m.size < mediaConstraints.getGifMaxSize(
context
)) ||
(MediaUtil.isVideoType(m.mimeType) && m.size < mediaConstraints.getVideoMaxSize(
context
))
}).toList()
}
override fun onCleared() {
if (!sentMedia) {
Stream.of(selectedMediaOrDefault)
.map({ obj: Media -> obj.uri })
.filter({ uri: Uri? ->
BlobProvider.isAuthority(
uri!!
)
})
.forEach({ uri: Uri? ->
BlobProvider.getInstance().delete(
application.applicationContext, uri!!
)
})
}
}
internal enum class Error {
ITEM_TOO_LARGE, TOO_MANY_ITEMS
}
internal class CountButtonState(val count: Int, private val visibility: Visibility) {
val isVisible: Boolean
get() {
when (visibility) {
Visibility.FORCED_ON -> return true
Visibility.FORCED_OFF -> return false
Visibility.CONDITIONAL -> return count > 0
else -> return false
}
}
internal enum class Visibility {
CONDITIONAL, FORCED_ON, FORCED_OFF
}
}
companion object {
private val TAG: String = MediaSendViewModel::class.java.simpleName
// the maximum amount of files that can be selected to send as attachment
const val MAX_SELECTED_FILES: Int = 32
}
}
Loading…
Cancel
Save