Konverting the MediaSendViewModel (#922)

* Konverting the MediaSendViewModel

* PR Feedback
pull/1710/head
ThomasSession 2 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.transition.Transition;
import dagger.hilt.android.AndroidEntryPoint;
import network.loki.messenger.R;
import org.session.libsignal.utilities.Log;
import com.bumptech.glide.Glide;
@ -43,6 +44,7 @@ import org.session.libsession.utilities.TextSecurePreferences;
import java.io.ByteArrayOutputStream;
@AndroidEntryPoint
public class Camera1Fragment extends Fragment implements TextureView.SurfaceTextureListener,
Camera1Controller.EventListener
{
@ -80,7 +82,7 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
controller = (Controller) getActivity();
camera = new Camera1Controller(TextSecurePreferences.getDirectCaptureCameraId(getContext()), displaySize.x, displaySize.y, this);
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

@ -28,11 +28,13 @@ import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.guava.Optional;
import dagger.hilt.android.AndroidEntryPoint;
import network.loki.messenger.R;
/**
* Allows the user to select a media folder to explore.
*/
@AndroidEntryPoint
public class MediaPickerFolderFragment extends Fragment implements MediaPickerFolderAdapter.EventListener {
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) {
super.onCreate(savedInstanceState);
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

@ -27,11 +27,13 @@ import org.session.libsession.utilities.Util;
import java.util.ArrayList;
import java.util.List;
import dagger.hilt.android.AndroidEntryPoint;
import network.loki.messenger.R;
/**
* Allows the user to select a set of media items from a specified folder.
*/
@AndroidEntryPoint
public class MediaPickerItemFragment extends Fragment implements MediaPickerItemAdapter.EventListener {
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);
folderTitle = getArguments().getString(KEY_FOLDER_TITLE);
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

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

@ -36,6 +36,8 @@ 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;
@ -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.
*/
@AndroidEntryPoint
public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGlobalLayoutListener,
MediaRailAdapter.RailItemListener,
InputAwareLayout.OnKeyboardShownListener,
@ -108,6 +111,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
}
controller = (Controller) requireActivity();
viewModel = new ViewModelProvider(requireActivity()).get(MediaSendViewModel.class);
}
@Override
@ -264,8 +268,6 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
}
private void initViewModel() {
viewModel = new ViewModelProvider(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
viewModel.getSelectedMedia().observe(this, media -> {
if (Util.isEmpty(media)) {
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