package org.thoughtcrime.securesms.mediasend; import android.app.Application; import android.arch.lifecycle.LiveData; import android.arch.lifecycle.MutableLiveData; import android.arch.lifecycle.ViewModel; import android.arch.lifecycle.ViewModelProvider; import android.content.Context; import android.net.Uri; import android.support.annotation.NonNull; import android.text.TextUtils; import com.annimon.stream.Stream; import org.thoughtcrime.securesms.TransportOption; import org.thoughtcrime.securesms.logging.Log; 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 org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; /** * 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_PUSH = 32; private static final int MAX_SMS = 1; 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 MediaConstraints mediaConstraints; private CharSequence body; private CountButtonState.Visibility countButtonVisibility; private boolean sentMedia; private Optional lastImageCapture; private int maxSelection; 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 setTransport(@NonNull TransportOption transport) { if (transport.isSms()) { maxSelection = MAX_SMS; mediaConstraints = MediaConstraints.getMmsMediaConstraints(transport.getSimSubscriptionId().or(-1)); } else { maxSelection = MAX_PUSH; mediaConstraints = MediaConstraints.getPushMediaConstraints(); } } 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() > maxSelection) { filteredMedia = filteredMedia.subList(0, maxSelection); 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); } countButtonVisibility = CountButtonState.Visibility.FORCED_OFF; bucketId.setValue(filteredMedia.get(0).getBucketId().or(Media.ALL_MEDIA_BUCKET_ID)); 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() >= maxSelection) { 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 onCaptionChanged(@NonNull String newCaption) { if (position.getValue() >= 0 && !Util.isEmpty(selectedMedia.getValue())) { selectedMedia.getValue().get(position.getValue()).setCaption(TextUtils.isEmpty(newCaption) ? null : newCaption); } } 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 maxSelection; } 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)); } } }