2018-11-20 09:59:23 -08:00
|
|
|
package org.thoughtcrime.securesms.mediasend;
|
|
|
|
|
2019-03-13 16:05:25 -07:00
|
|
|
import android.app.Application;
|
2018-11-20 09:59:23 -08:00
|
|
|
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;
|
|
|
|
|
2019-03-14 17:01:23 -07:00
|
|
|
import org.thoughtcrime.securesms.TransportOption;
|
2019-03-19 17:32:55 -07:00
|
|
|
import org.thoughtcrime.securesms.logging.Log;
|
2019-02-11 15:05:37 -08:00
|
|
|
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
2019-03-13 16:05:25 -07:00
|
|
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
2019-01-16 13:32:39 -08:00
|
|
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
2019-02-11 15:05:37 -08:00
|
|
|
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
2018-11-20 09:59:23 -08:00
|
|
|
import org.thoughtcrime.securesms.util.Util;
|
2019-03-13 16:05:25 -07:00
|
|
|
import org.whispersystems.libsignal.util.guava.Optional;
|
2018-11-20 09:59:23 -08:00
|
|
|
|
|
|
|
import java.util.Collections;
|
|
|
|
import java.util.HashMap;
|
2019-03-13 16:05:25 -07:00
|
|
|
import java.util.LinkedList;
|
2018-11-20 09:59:23 -08:00
|
|
|
import java.util.List;
|
|
|
|
import java.util.Map;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Manages the observable datasets available in {@link MediaSendActivity}.
|
|
|
|
*/
|
|
|
|
class MediaSendViewModel extends ViewModel {
|
|
|
|
|
2019-03-19 17:32:55 -07:00
|
|
|
private static final String TAG = MediaSendViewModel.class.getSimpleName();
|
|
|
|
|
2019-03-14 17:01:23 -07:00
|
|
|
private static final int MAX_PUSH = 32;
|
|
|
|
private static final int MAX_SMS = 1;
|
|
|
|
|
2019-03-13 16:05:25 -07:00
|
|
|
private final Application application;
|
2018-11-20 09:59:23 -08:00
|
|
|
private final MediaRepository repository;
|
|
|
|
private final MutableLiveData<List<Media>> selectedMedia;
|
|
|
|
private final MutableLiveData<List<Media>> bucketMedia;
|
|
|
|
private final MutableLiveData<Integer> position;
|
2019-03-01 10:50:48 -08:00
|
|
|
private final MutableLiveData<String> bucketId;
|
2018-11-20 09:59:23 -08:00
|
|
|
private final MutableLiveData<List<MediaFolder>> folders;
|
2019-03-01 10:50:48 -08:00
|
|
|
private final MutableLiveData<CountButtonState> countButtonState;
|
2019-03-14 17:01:23 -07:00
|
|
|
private final MutableLiveData<Boolean> cameraButtonVisibility;
|
2019-02-11 15:05:37 -08:00
|
|
|
private final SingleLiveEvent<Error> error;
|
2018-11-20 09:59:23 -08:00
|
|
|
private final Map<Uri, Object> savedDrawState;
|
|
|
|
|
2019-03-01 10:50:48 -08:00
|
|
|
private MediaConstraints mediaConstraints;
|
|
|
|
private CharSequence body;
|
|
|
|
private CountButtonState.Visibility countButtonVisibility;
|
2019-03-13 16:05:25 -07:00
|
|
|
private boolean sentMedia;
|
|
|
|
private Optional<Media> lastImageCapture;
|
2019-03-14 17:01:23 -07:00
|
|
|
private int maxSelection;
|
2019-02-11 15:05:37 -08:00
|
|
|
|
2019-03-13 16:05:25 -07:00
|
|
|
private MediaSendViewModel(@NonNull Application application, @NonNull MediaRepository repository) {
|
2019-03-14 17:01:23 -07:00
|
|
|
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<>();
|
2019-03-19 16:11:46 -07:00
|
|
|
this.countButtonVisibility = CountButtonState.Visibility.FORCED_OFF;
|
2019-03-14 17:01:23 -07:00
|
|
|
this.lastImageCapture = Optional.absent();
|
|
|
|
this.body = "";
|
2018-11-20 09:59:23 -08:00
|
|
|
|
|
|
|
position.setValue(-1);
|
2019-03-19 16:11:46 -07:00
|
|
|
countButtonState.setValue(new CountButtonState(0, countButtonVisibility));
|
2019-03-14 17:01:23 -07:00
|
|
|
cameraButtonVisibility.setValue(false);
|
2018-11-20 09:59:23 -08:00
|
|
|
}
|
|
|
|
|
2019-03-14 17:01:23 -07:00
|
|
|
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();
|
|
|
|
}
|
2019-02-11 15:05:37 -08:00
|
|
|
}
|
|
|
|
|
2019-03-01 10:50:48 -08:00
|
|
|
void onSelectedMediaChanged(@NonNull Context context, @NonNull List<Media> newMedia) {
|
2019-02-11 15:05:37 -08:00
|
|
|
repository.getPopulatedMedia(context, newMedia, populatedMedia -> {
|
2019-03-19 16:11:46 -07:00
|
|
|
Util.runOnMain(() -> {
|
|
|
|
|
|
|
|
List<Media> 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<Media> 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));
|
|
|
|
});
|
2019-02-11 15:05:37 -08:00
|
|
|
});
|
2018-11-20 09:59:23 -08:00
|
|
|
}
|
|
|
|
|
2019-03-01 10:50:48 -08:00
|
|
|
void onMultiSelectStarted() {
|
|
|
|
countButtonVisibility = CountButtonState.Visibility.FORCED_ON;
|
2019-03-13 16:05:25 -07:00
|
|
|
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
|
2019-03-01 10:50:48 -08:00
|
|
|
}
|
2019-02-11 15:05:37 -08:00
|
|
|
|
2019-03-01 10:50:48 -08:00
|
|
|
void onImageEditorStarted() {
|
|
|
|
countButtonVisibility = CountButtonState.Visibility.FORCED_OFF;
|
2019-03-13 16:05:25 -07:00
|
|
|
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
|
2019-03-14 17:01:23 -07:00
|
|
|
cameraButtonVisibility.setValue(false);
|
2019-03-01 10:50:48 -08:00
|
|
|
}
|
|
|
|
|
2019-03-19 16:11:46 -07:00
|
|
|
void onCameraStarted() {
|
2019-03-01 10:50:48 -08:00
|
|
|
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
|
2019-03-13 16:05:25 -07:00
|
|
|
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
|
2019-03-14 17:01:23 -07:00
|
|
|
cameraButtonVisibility.setValue(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
void onItemPickerStarted() {
|
2019-03-19 16:11:46 -07:00
|
|
|
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
|
|
|
|
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
|
2019-03-14 17:01:23 -07:00
|
|
|
cameraButtonVisibility.setValue(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
void onFolderPickerStarted() {
|
2019-03-19 16:11:46 -07:00
|
|
|
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
|
|
|
|
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
|
2019-03-14 17:01:23 -07:00
|
|
|
cameraButtonVisibility.setValue(true);
|
|
|
|
}
|
|
|
|
|
2019-03-01 10:50:48 -08:00
|
|
|
void onBodyChanged(@NonNull CharSequence body) {
|
|
|
|
this.body = body;
|
2018-11-20 09:59:23 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
void onFolderSelected(@NonNull String bucketId) {
|
2019-03-01 10:50:48 -08:00
|
|
|
this.bucketId.setValue(bucketId);
|
2018-11-20 09:59:23 -08:00
|
|
|
bucketMedia.setValue(Collections.emptyList());
|
|
|
|
}
|
|
|
|
|
|
|
|
void onPageChanged(int position) {
|
2019-03-19 17:32:55 -07:00
|
|
|
if (position < 0 || position >= getSelectedMediaOrDefault().size()) {
|
|
|
|
Log.w(TAG, "Tried to move to an out-of-bounds item. Size: " + getSelectedMediaOrDefault().size() + ", position: " + position);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-11-20 09:59:23 -08:00
|
|
|
this.position.setValue(position);
|
|
|
|
}
|
|
|
|
|
2019-03-13 16:05:25 -07:00
|
|
|
void onMediaItemRemoved(@NonNull Context context, int position) {
|
2019-03-19 17:32:55 -07:00
|
|
|
if (position < 0 || position >= getSelectedMediaOrDefault().size()) {
|
|
|
|
Log.w(TAG, "Tried to remove an out-of-bounds item. Size: " + getSelectedMediaOrDefault().size() + ", position: " + position);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-03-13 16:05:25 -07:00
|
|
|
Media removed = getSelectedMediaOrDefault().remove(position);
|
|
|
|
|
|
|
|
if (removed != null && BlobProvider.isAuthority(removed.getUri())) {
|
|
|
|
BlobProvider.getInstance().delete(context, removed.getUri());
|
|
|
|
}
|
|
|
|
|
2018-11-20 09:59:23 -08:00
|
|
|
selectedMedia.setValue(selectedMedia.getValue());
|
|
|
|
}
|
|
|
|
|
2019-03-13 16:05:25 -07:00
|
|
|
void onImageCaptured(@NonNull Media media) {
|
|
|
|
List<Media> selected = selectedMedia.getValue();
|
|
|
|
|
|
|
|
if (selected == null) {
|
|
|
|
selected = new LinkedList<>();
|
|
|
|
}
|
|
|
|
|
2019-03-14 17:01:23 -07:00
|
|
|
if (selected.size() >= maxSelection) {
|
2019-03-19 16:11:46 -07:00
|
|
|
error.setValue(Error.TOO_MANY_ITEMS);
|
2019-03-14 17:01:23 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-03-13 16:05:25 -07:00
|
|
|
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());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-11-20 09:59:23 -08:00
|
|
|
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<Uri, Object> state) {
|
|
|
|
savedDrawState.clear();
|
|
|
|
savedDrawState.putAll(state);
|
|
|
|
}
|
|
|
|
|
2019-03-13 16:05:25 -07:00
|
|
|
void onSendClicked() {
|
|
|
|
sentMedia = true;
|
|
|
|
}
|
|
|
|
|
2018-11-20 09:59:23 -08:00
|
|
|
@NonNull Map<Uri, Object> getDrawState() {
|
|
|
|
return savedDrawState;
|
|
|
|
}
|
|
|
|
|
2019-03-01 10:50:48 -08:00
|
|
|
@NonNull LiveData<List<Media>> getSelectedMedia() {
|
2018-11-20 09:59:23 -08:00
|
|
|
return selectedMedia;
|
|
|
|
}
|
|
|
|
|
2019-03-01 10:50:48 -08:00
|
|
|
@NonNull LiveData<List<Media>> getMediaInBucket(@NonNull Context context, @NonNull String bucketId) {
|
2018-11-20 09:59:23 -08:00
|
|
|
repository.getMediaInBucket(context, bucketId, bucketMedia::postValue);
|
|
|
|
return bucketMedia;
|
|
|
|
}
|
|
|
|
|
|
|
|
@NonNull LiveData<List<MediaFolder>> getFolders(@NonNull Context context) {
|
|
|
|
repository.getFolders(context, folders::postValue);
|
|
|
|
return folders;
|
|
|
|
}
|
|
|
|
|
2019-03-01 10:50:48 -08:00
|
|
|
@NonNull LiveData<CountButtonState> getCountButtonState() {
|
|
|
|
return countButtonState;
|
|
|
|
}
|
|
|
|
|
2019-03-14 17:01:23 -07:00
|
|
|
@NonNull LiveData<Boolean> getCameraButtonVisibility() {
|
|
|
|
return cameraButtonVisibility;
|
|
|
|
}
|
|
|
|
|
|
|
|
@NonNull CharSequence getBody() {
|
2019-03-01 10:50:48 -08:00
|
|
|
return body;
|
|
|
|
}
|
|
|
|
|
2019-03-14 17:01:23 -07:00
|
|
|
@NonNull LiveData<Integer> getPosition() {
|
2018-11-20 09:59:23 -08:00
|
|
|
return position;
|
|
|
|
}
|
|
|
|
|
2019-03-14 17:01:23 -07:00
|
|
|
@NonNull LiveData<String> getBucketId() {
|
2018-11-20 09:59:23 -08:00
|
|
|
return bucketId;
|
|
|
|
}
|
|
|
|
|
2019-03-14 17:01:23 -07:00
|
|
|
@NonNull LiveData<Error> getError() {
|
2019-02-11 15:05:37 -08:00
|
|
|
return error;
|
|
|
|
}
|
|
|
|
|
2019-03-14 17:01:23 -07:00
|
|
|
int getMaxSelection() {
|
|
|
|
return maxSelection;
|
|
|
|
}
|
|
|
|
|
2019-03-01 10:50:48 -08:00
|
|
|
private @NonNull List<Media> getSelectedMediaOrDefault() {
|
|
|
|
return selectedMedia.getValue() == null ? Collections.emptyList()
|
|
|
|
: selectedMedia.getValue();
|
2018-11-20 09:59:23 -08:00
|
|
|
}
|
|
|
|
|
2019-02-11 15:05:37 -08:00
|
|
|
private @NonNull List<Media> getFilteredMedia(@NonNull Context context, @NonNull List<Media> media, @NonNull MediaConstraints mediaConstraints) {
|
2019-01-16 13:32:39 -08:00
|
|
|
return Stream.of(media).filter(m -> MediaUtil.isGif(m.getMimeType()) ||
|
|
|
|
MediaUtil.isImageType(m.getMimeType()) ||
|
2019-02-11 15:05:37 -08:00
|
|
|
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();
|
|
|
|
|
|
|
|
}
|
2019-01-16 13:32:39 -08:00
|
|
|
|
2019-03-13 16:05:25 -07:00
|
|
|
@Override
|
|
|
|
protected void onCleared() {
|
|
|
|
if (!sentMedia) {
|
|
|
|
Stream.of(getSelectedMediaOrDefault())
|
|
|
|
.map(Media::getUri)
|
|
|
|
.filter(BlobProvider::isAuthority)
|
|
|
|
.forEach(uri -> BlobProvider.getInstance().delete(application.getApplicationContext(), uri));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-11 15:05:37 -08:00
|
|
|
enum Error {
|
2019-03-14 17:01:23 -07:00
|
|
|
ITEM_TOO_LARGE, TOO_MANY_ITEMS
|
2019-01-16 13:32:39 -08:00
|
|
|
}
|
|
|
|
|
2019-03-01 10:50:48 -08:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2019-03-14 17:01:23 -07:00
|
|
|
boolean isVisible() {
|
2019-03-01 10:50:48 -08:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-20 09:59:23 -08:00
|
|
|
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
|
|
|
|
2019-03-13 16:05:25 -07:00
|
|
|
private final Application application;
|
2018-11-20 09:59:23 -08:00
|
|
|
private final MediaRepository repository;
|
|
|
|
|
2019-03-13 16:05:25 -07:00
|
|
|
Factory(@NonNull Application application, @NonNull MediaRepository repository) {
|
|
|
|
this.application = application;
|
|
|
|
this.repository = repository;
|
2018-11-20 09:59:23 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
2019-03-13 16:05:25 -07:00
|
|
|
return modelClass.cast(new MediaSendViewModel(application, repository));
|
2018-11-20 09:59:23 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|