gt:       List<Media> updatedMedia = new ArrayList<>(oldToNew.values());
context: package org.thoughtcrime.securesms.mediasend;
  
  import android.app.Application;
  import android.content.Context;
  import android.net.Uri;
  import android.text.TextUtils;
  
  import androidx.annotation.NonNull;
  import androidx.annotation.Nullable;
  import androidx.annotation.WorkerThread;
  import androidx.lifecycle.LiveData;
  import androidx.lifecycle.MutableLiveData;
  import androidx.lifecycle.ViewModel;
  import androidx.lifecycle.ViewModelProvider;
  
  import com.annimon.stream.Stream;
  
  import org.signal.core.util.ThreadUtil;
  import org.signal.core.util.logging.Log;
  import org.thoughtcrime.securesms.TransportOption;
  import org.thoughtcrime.securesms.database.ThreadDatabase;
  import org.thoughtcrime.securesms.database.model.Mention;
  import org.thoughtcrime.securesms.mms.MediaConstraints;
  import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
  import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
  import org.thoughtcrime.securesms.mms.SentMediaQuality;
  import org.thoughtcrime.securesms.mms.Slide;
  import org.thoughtcrime.securesms.providers.BlobProvider;
  import org.thoughtcrime.securesms.recipients.Recipient;
  import org.thoughtcrime.securesms.sms.MessageSender;
  import org.thoughtcrime.securesms.sms.MessageSender.PreUploadResult;
  import org.thoughtcrime.securesms.util.DiffHelper;
  import org.thoughtcrime.securesms.util.MediaUtil;
  import org.thoughtcrime.securesms.util.MessageUtil;
  import org.thoughtcrime.securesms.util.SingleLiveEvent;
  import org.thoughtcrime.securesms.util.TextSecurePreferences;
  import org.thoughtcrime.securesms.util.Util;
  import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
  import org.whispersystems.libsignal.util.guava.Optional;
  import org.whispersystems.libsignal.util.guava.Preconditions;
  
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.Map;
  import java.util.Objects;
  
  /**
   * Manages the observable datasets available in {@link MediaSendActivity}.
   */
  class MediaSendViewModel extends ViewModel {
  
    private static final String TAG = Log.tag(MediaSendViewModel.class);
  
    private final Application                        application;
    private final MediaRepository                    repository;
    private final MediaUploadRepository              uploadRepository;
    private final MutableLiveData<List<Media>>       selectedMedia;
    private final MutableLiveData<List<Media>>       bucketMedia;
    private final MutableLiveData<Optional<Media>>   mostRecentMedia;
    private final MutableLiveData<Integer>           position;
    private final MutableLiveData<String>            bucketId;
    private final MutableLiveData<List<MediaFolder>> folders;
    private final MutableLiveData<HudState>          hudState;
    private final SingleLiveEvent<Error>             error;
    private final SingleLiveEvent<Event>             event;
    private final MutableLiveData<SentMediaQuality>  sentMediaQuality;
    private final LiveData<Boolean>                  showMediaQualityToggle;
    private final Map<Uri, Object>                   savedDrawState;
  
    private TransportOption  transport;
    private MediaConstraints mediaConstraints;
    private CharSequence     body;
    private boolean          sentMedia;
    private int              maxSelection;
    private Page             page;
    private boolean          isSms;
    private boolean          meteredConnection;
    private Optional<Media>  lastCameraCapture;
    private boolean          preUploadEnabled;
  
    private boolean       hudVisible;
    private boolean       composeVisible;
    private boolean       captionVisible;
    private ButtonState   buttonState;
    private RailState     railState;
    private ViewOnceState viewOnceState;
  
    private @Nullable Recipient recipient;
  
    private MediaSendViewModel(@NonNull Application application,
                               @NonNull MediaRepository repository,
                               @NonNull MediaUploadRepository uploadRepository)
    {
      this.application            = application;
      this.repository             = repository;
      this.uploadRepository       = uploadRepository;
      this.selectedMedia          = new MutableLiveData<>();
      this.bucketMedia            = new MutableLiveData<>();
      this.mostRecentMedia        = new MutableLiveData<>();
      this.position               = new MutableLiveData<>();
      this.bucketId               = new MutableLiveData<>();
      this.folders                = new MutableLiveData<>();
      this.hudState               = new MutableLiveData<>();
      this.error                  = new SingleLiveEvent<>();
      this.event                  = new SingleLiveEvent<>();
      this.sentMediaQuality       = new MutableLiveData<>(SentMediaQuality.STANDARD);
      this.savedDrawState         = new HashMap<>();
      this.lastCameraCapture      = Optional.absent();
      this.body                   = "";
      this.buttonState            = ButtonState.GONE;
      this.railState              = RailState.GONE;
      this.viewOnceState          = ViewOnceState.GONE;
      this.page                   = Page.UNKNOWN;
      this.preUploadEnabled       = true;
      this.showMediaQualityToggle = LiveDataUtil.mapAsync(this.selectedMedia, s -> s.stream().anyMatch(m -> MediaUtil.isImageAndNotGif(m.getMimeType())));
  
      position.setValue(-1);
    }
  
    void setTransport(@NonNull TransportOption transport) {
      this.transport = transport;
  
      if (transport.isSms()) {
        isSms            = true;
        maxSelection     = MediaSendConstants.MAX_SMS;
        mediaConstraints = MediaConstraints.getMmsMediaConstraints(transport.getSimSubscriptionId().or(-1));
      } else {
        isSms            = false;
        maxSelection     = MediaSendConstants.MAX_PUSH;
        mediaConstraints = MediaConstraints.getPushMediaConstraints();
      }
  
      preUploadEnabled = shouldPreUpload(application, meteredConnection, isSms, recipient);
    }
  
    void setRecipient(@Nullable Recipient recipient) {
      this.recipient        = recipient;
      this.preUploadEnabled = shouldPreUpload(application, meteredConnection, isSms, recipient);
    }
  
    void onSelectedMediaChanged(@NonNull Context context, @NonNull List<Media> newMedia) {
      List<Media> originalMedia = getSelectedMediaOrDefault();
  
      repository.getPopulatedMedia(context, newMedia, populatedMedia -> {
        ThreadUtil.runOnMain(() -> {
          List<Media> filteredMedia = getFilteredMedia(context, populatedMedia, mediaConstraints);
  
          if (filteredMedia.size() != newMedia.size()) {
            if (filteredMedia.isEmpty() && newMedia.size() == 1 && page == Page.UNKNOWN) {
              if (MediaUtil.isImageOrVideoType(newMedia.get(0).getMimeType())) {
                error.setValue(Error.ONLY_ITEM_TOO_LARGE);
              } else {
                error.setValue(Error.ONLY_ITEM_IS_INVALID_TYPE);
              }
            } else {
              if (newMedia.stream().allMatch(m -> MediaUtil.isImageOrVideoType(m.getMimeType()))) {
                error.setValue(Error.ITEM_TOO_LARGE);
              } else {
                error.setValue(Error.ITEM_TOO_LARGE_OR_INVALID_TYPE);
              }
            }
          }
  
          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);
          }
  
          if (page == Page.EDITOR && filteredMedia.isEmpty()) {
            error.postValue(Error.NO_ITEMS);
          } else if (filteredMedia.isEmpty()) {
            hudVisible = false;
            selectedMedia.setValue(filteredMedia);
            hudState.setValue(buildHudState());
          } else {
            hudVisible = true;
            selectedMedia.setValue(filteredMedia);
            hudState.setValue(buildHudState());
          }
  
          updateAttachmentUploads(originalMedia, getSelectedMediaOrDefault());
        });
      });
    }
  
    void onSingleMediaSelected(@NonNull Context context, @NonNull Media media) {
      repository.getPopulatedMedia(context, Collections.singletonList(media), populatedMedia -> {
        ThreadUtil.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));
          }
  
          selectedMedia.setValue(filteredMedia);
        });
      });
    }
  
    void onMultiSelectStarted() {
      hudVisible     = true;
      composeVisible = false;
      captionVisible = false;
      buttonState    = ButtonState.COUNT;
      railState      = RailState.VIEWABLE;
      viewOnceState  = ViewOnceState.GONE;
  
      hudState.setValue(buildHudState());
    }
  
    void onImageEditorStarted() {
      page           = Page.EDITOR;
      hudVisible     = true;
      captionVisible = getSelectedMediaOrDefault().size() > 1 || (getSelectedMediaOrDefault().size() > 0 && getSelectedMediaOrDefault().get(0).getCaption().isPresent());
      buttonState    = (recipient != null) ? ButtonState.SEND : ButtonState.CONTINUE;
  
      updateViewOnceState();
      showViewOnceTooltipIfNecessary(viewOnceState);
  
      railState      = !isSms && viewOnceState != ViewOnceState.ENABLED ? RailState.INTERACTIVE : RailState.GONE;
      composeVisible = viewOnceState != ViewOnceState.ENABLED;
  
      hudState.setValue(buildHudState());
    }
  
    void onCameraStarted() {
      // TODO: Don't need this?
      Page previous = page;
  
      page          = Page.CAMERA;
      hudVisible    = false;
      viewOnceState = ViewOnceState.GONE;
      buttonState   = ButtonState.COUNT;
  
      List<Media> selected = getSelectedMediaOrDefault();
  
      if (previous == Page.EDITOR && lastCameraCapture.isPresent() && selected.contains(lastCameraCapture.get()) && selected.size() == 1) {
        selected.remove(lastCameraCapture.get());
        selectedMedia.setValue(selected);
        BlobProvider.getInstance().delete(application, lastCameraCapture.get().getUri());
        cancelUpload(lastCameraCapture.get());
      }
  
      hudState.setValue(buildHudState());
    }
  
    void onItemPickerStarted() {
      page           = Page.ITEM_PICKER;
      hudVisible     = true;
      composeVisible = false;
      captionVisible = false;
      buttonState    = ButtonState.COUNT;
      viewOnceState  = ViewOnceState.GONE;
      railState      = getSelectedMediaOrDefault().isEmpty() ? RailState.GONE : RailState.VIEWABLE;
  
      lastCameraCapture = Optional.absent();
  
      hudState.setValue(buildHudState());
    }
  
    void onFolderPickerStarted() {
      page           = Page.FOLDER_PICKER;
      hudVisible     = true;
      composeVisible = false;
      captionVisible = false;
      buttonState    = ButtonState.COUNT;
      viewOnceState  = ViewOnceState.GONE;
      railState      = getSelectedMediaOrDefault().isEmpty() ? RailState.GONE : RailState.VIEWABLE;
  
      lastCameraCapture = Optional.absent();
  
      hudState.setValue(buildHudState());
    }
  
    void onContactSelectStarted() {
      hudVisible = false;
  
      hudState.setValue(buildHudState());
    }
  
    void onRevealButtonToggled() {
      hudVisible     = true;
      viewOnceState  = viewOnceState == ViewOnceState.ENABLED ? ViewOnceState.DISABLED : ViewOnceState.ENABLED;
      composeVisible = viewOnceState != ViewOnceState.ENABLED;
      railState      = viewOnceState == ViewOnceState.ENABLED || isSms ? RailState.GONE : RailState.INTERACTIVE;
      captionVisible = false;
  
      List<Media> uncaptioned = Stream.of(getSelectedMediaOrDefault())
                                      .map(m -> new Media(m.getUri(), m.getMimeType(), m.getDate(), m.getWidth(), m.getHeight(), m.getSize(), m.getDuration(), m.isBorderless(), m.isVideoGif(), m.getBucketId(), Optional.absent(), Optional.absent()))
                                      .toList();
  
      selectedMedia.setValue(uncaptioned);
      position.setValue(position.getValue() != null ? position.getValue() : 0);
      hudState.setValue(buildHudState());
    }
  
    void onKeyboardHidden(boolean isSms) {
      if (page != Page.EDITOR) return;
  
      composeVisible = (viewOnceState != ViewOnceState.ENABLED);
      buttonState    = (recipient != null) ? ButtonState.SEND : ButtonState.CONTINUE;
  
      if (isSms) {
        railState      = RailState.GONE;
        captionVisible = false;
      } else {
        railState = viewOnceState != ViewOnceState.ENABLED ? RailState.INTERACTIVE : RailState.GONE;
  
        if (getSelectedMediaOrDefault().size() > 1 || (getSelectedMediaOrDefault().size() > 0 && getSelectedMediaOrDefault().get(0).getCaption().isPresent())) {
          captionVisible = true;
        }
      }
  
      hudState.setValue(buildHudState());
    }
  
    void onKeyboardShown(boolean isComposeFocused, boolean isCaptionFocused, boolean isSms) {
      if (page != Page.EDITOR) return;
  
      if (isSms) {
        railState      = RailState.GONE;
        composeVisible = (viewOnceState == ViewOnceState.GONE);
        captionVisible = false;
        buttonState    = (recipient != null) ? ButtonState.SEND : ButtonState.CONTINUE;
      } else {
        if (isCaptionFocused) {
          railState      = viewOnceState != ViewOnceState.ENABLED ? RailState.INTERACTIVE : RailState.GONE;
          composeVisible = false;
          captionVisible = true;
          buttonState    = ButtonState.GONE;
        } else if (isComposeFocused) {
          railState      = viewOnceState != ViewOnceState.ENABLED ? RailState.INTERACTIVE : RailState.GONE;
          composeVisible = (viewOnceState != ViewOnceState.ENABLED);
          captionVisible = false;
          buttonState    = (recipient != null) ? ButtonState.SEND : ButtonState.CONTINUE;
        }
      }
  
      hudState.setValue(buildHudState());
    }
  
    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());
      }
  
      cancelUpload(removed);
  
      if (page == Page.EDITOR && getSelectedMediaOrDefault().isEmpty()) {
        error.setValue(Error.NO_ITEMS);
      } else {
        selectedMedia.setValue(selectedMedia.getValue());
      }
  
      if (getSelectedMediaOrDefault().size() > 0) {
        this.position.setValue(Math.min(position, getSelectedMediaOrDefault().size() - 1));
      }
  
      if (getSelectedMediaOrDefault().size() == 1) {
        viewOnceState = viewOnceSupported() ? ViewOnceState.DISABLED : ViewOnceState.GONE;
      }
  
      hudState.setValue(buildHudState());
    }
  
    void onVideoBeginEdit(@NonNull Uri uri) {
      cancelUpload(new Media(uri, "", 0, 0, 0, 0, 0, false, false, Optional.absent(), Optional.absent(), Optional.absent()));
    }
  
    void onMediaCaptured(@NonNull Media media) {
      lastCameraCapture = Optional.of(media);
  
      List<Media> selected = selectedMedia.getValue();
  
      if (selected == null) {
        selected = new LinkedList<>();
      }
  
      if (selected.size() >= maxSelection) {
        error.setValue(Error.TOO_MANY_ITEMS);
        return;
      }
  
      selected.add(media);
      selectedMedia.setValue(selected);
      position.setValue(selected.size() - 1);
      bucketId.setValue(Media.ALL_MEDIA_BUCKET_ID);
  
      startUpload(media);
    }
  
    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 onCameraControlsInitialized() {
      repository.getMostRecentItem(application, mostRecentMedia::postValue);
    }
  
    void onMeteredConnectivityStatusChanged(boolean metered) {
      Log.i(TAG, "Metered connectivity status set to: " + metered);
  
      meteredConnection = metered;
      preUploadEnabled  = shouldPreUpload(application, metered, isSms, recipient);
    }
  
    void saveDrawState(@NonNull Map<Uri, Object> state) {
      savedDrawState.clear();
      savedDrawState.putAll(state);
    }
  
    public void setSentMediaQuality(@NonNull SentMediaQuality newQuality) {
      if (newQuality == sentMediaQuality.getValue()) {
        return;
      }
  
      sentMediaQuality.setValue(newQuality);
      preUploadEnabled = false;
      uploadRepository.cancelAllUploads();
    }
  
    @NonNull LiveData<MediaSendActivityResult> onSendClicked(Map<Media, MediaTransform> modelsToTransform, @NonNull List<Recipient> recipients, @NonNull List<Mention> mentions) {
      if (isSms && recipients.size() > 0) {
        throw new IllegalStateException("Provided recipients to send to, but this is SMS!");
      }
  
      MutableLiveData<MediaSendActivityResult> result          = new MutableLiveData<>();
      String                                   trimmedBody     = isViewOnce() ? "" : body.toString().trim();
      List<Media>                              initialMedia    = getSelectedMediaOrDefault();
      List<Mention>                            trimmedMentions = isViewOnce() ? Collections.emptyList() : mentions;
  
      Preconditions.checkState(initialMedia.size() > 0, "No media to send!");
  
      MediaRepository.transformMedia(application, initialMedia, modelsToTransform, (oldToNew) -> {
  