diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/Camera1Fragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/Camera1Fragment.java index 3ef7090c30..70eb0ba3ed 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/Camera1Fragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/Camera1Fragment.java @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java index 82d6b93085..c2eb2e2b24 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java index b1c104e32e..c9532e6d05 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt index 26f50bcf9b..215bff717e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt @@ -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? = 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, 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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java index 633555c672..610f4ff140 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java @@ -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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java deleted file mode 100644 index 0220dd9027..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java +++ /dev/null @@ -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> selectedMedia; - private final MutableLiveData> bucketMedia; - private final MutableLiveData position; - private final MutableLiveData bucketId; - private final MutableLiveData> folders; - private final MutableLiveData countButtonState; - private final MutableLiveData cameraButtonVisibility; - private final SingleLiveEvent error; - private final Map savedDrawState; - - private final MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints(); - - private CharSequence body; - private CountButtonState.Visibility countButtonVisibility; - private boolean sentMedia; - private Optional 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 newMedia) { - repository.getPopulatedMedia(context, newMedia, populatedMedia -> { - Util.runOnMain(() -> { - - List 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 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 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 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 state) { - savedDrawState.clear(); - savedDrawState.putAll(state); - } - - void onSendClicked() { - sentMedia = true; - } - - @NonNull Map getDrawState() { - return savedDrawState; - } - - @NonNull LiveData> getSelectedMedia() { - return selectedMedia; - } - - @NonNull LiveData> getMediaInBucket(@NonNull Context context, @NonNull String bucketId) { - repository.getMediaInBucket(context, bucketId, bucketMedia::postValue); - return bucketMedia; - } - - @NonNull LiveData> getFolders(@NonNull Context context) { - repository.getFolders(context, folders::postValue); - return folders; - } - - @NonNull LiveData getCountButtonState() { - return countButtonState; - } - - @NonNull LiveData getCameraButtonVisibility() { - return cameraButtonVisibility; - } - - @NonNull CharSequence getBody() { - return body; - } - - @NonNull LiveData getPosition() { - return position; - } - - @NonNull LiveData getBucketId() { - return bucketId; - } - - @NonNull LiveData getError() { - return error; - } - - int getMaxSelection() { - return MAX_SELECTION; - } - - private @NonNull List getSelectedMediaOrDefault() { - return selectedMedia.getValue() == null ? Collections.emptyList() - : selectedMedia.getValue(); - } - - private @NonNull List getFilteredMedia(@NonNull Context context, @NonNull List 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 create(@NonNull Class modelClass) { - return modelClass.cast(new MediaSendViewModel(application, repository)); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.kt new file mode 100644 index 0000000000..c5150b2974 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.kt @@ -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?> + private val bucketMedia: MutableLiveData> + private val position: MutableLiveData + private val bucketId: MutableLiveData + private val folders: MutableLiveData> + private val countButtonState: MutableLiveData + private val cameraButtonVisibility: MutableLiveData + private val error: SingleLiveEvent + private val savedDrawState: MutableMap + + 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 + + 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) { + repository.getPopulatedMedia(context, newMedia, + { populatedMedia: List -> + runOnMain( + { + var filteredMedia: List = + 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 -> + runOnMain( + { + val filteredMedia: List = + 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? = 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 = 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) { + savedDrawState.clear() + savedDrawState.putAll(state) + } + + fun onSendClicked() { + sentMedia = true + } + + val drawState: Map + get() = savedDrawState + + fun getSelectedMedia(): LiveData?> { + return selectedMedia + } + + fun getMediaInBucket(context: Context, bucketId: String): LiveData> { + repository.getMediaInBucket(context, bucketId, + { value: List -> bucketMedia.postValue(value) }) + return bucketMedia + } + + fun getFolders(context: Context): LiveData> { + repository.getFolders(context, + { value: List -> folders.postValue(value) }) + return folders + } + + fun getCountButtonState(): LiveData { + return countButtonState + } + + fun getCameraButtonVisibility(): LiveData { + return cameraButtonVisibility + } + + fun getPosition(): LiveData { + return position + } + + fun getBucketId(): LiveData { + return bucketId + } + + fun getError(): LiveData { + return error + } + + private val selectedMediaOrDefault: List + get() = if (selectedMedia.value == null) emptyList() else + selectedMedia.value!! + + private fun getFilteredMedia( + context: Context, + media: List, + mediaConstraints: MediaConstraints + ): List { + 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 + } +}