Merge branch 'dev' of github.com:session-foundation/session-android into dev
commit
f83db366b0
@ -1,446 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.mediasend;
|
|
||||||
|
|
||||||
import static org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY;
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
|
||||||
import android.view.animation.AccelerateInterpolator;
|
|
||||||
import android.view.animation.Animation;
|
|
||||||
import android.view.animation.DecelerateInterpolator;
|
|
||||||
import android.view.animation.OvershootInterpolator;
|
|
||||||
import android.view.animation.ScaleAnimation;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
|
||||||
import com.squareup.phrase.Phrase;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
import org.session.libsession.utilities.Address;
|
|
||||||
import org.session.libsession.utilities.MediaTypes;
|
|
||||||
import org.session.libsession.utilities.Util;
|
|
||||||
import org.session.libsession.utilities.concurrent.SimpleTask;
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
|
||||||
import org.session.libsignal.utilities.Log;
|
|
||||||
import org.session.libsignal.utilities.guava.Optional;
|
|
||||||
import org.thoughtcrime.securesms.ScreenLockActionBarActivity;
|
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
|
||||||
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment;
|
|
||||||
import org.thoughtcrime.securesms.util.FilenameUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encompasses the entire flow of sending media, starting from the selection process to the actual
|
|
||||||
* captioning and editing of the content.
|
|
||||||
*
|
|
||||||
* This activity is intended to be launched via {@link #startActivityForResult(Intent, int)}.
|
|
||||||
* It will return the {@link Media} that the user decided to send.
|
|
||||||
*/
|
|
||||||
public class MediaSendActivity extends ScreenLockActionBarActivity implements MediaPickerFolderFragment.Controller,
|
|
||||||
MediaPickerItemFragment.Controller,
|
|
||||||
MediaSendFragment.Controller,
|
|
||||||
ImageEditorFragment.Controller,
|
|
||||||
Camera1Fragment.Controller
|
|
||||||
{
|
|
||||||
private static final String TAG = MediaSendActivity.class.getSimpleName();
|
|
||||||
|
|
||||||
public static final String EXTRA_MEDIA = "media";
|
|
||||||
public static final String EXTRA_MESSAGE = "message";
|
|
||||||
|
|
||||||
private static final String KEY_ADDRESS = "address";
|
|
||||||
private static final String KEY_BODY = "body";
|
|
||||||
private static final String KEY_MEDIA = "media";
|
|
||||||
private static final String KEY_IS_CAMERA = "is_camera";
|
|
||||||
|
|
||||||
private static final String TAG_FOLDER_PICKER = "folder_picker";
|
|
||||||
private static final String TAG_ITEM_PICKER = "item_picker";
|
|
||||||
private static final String TAG_SEND = "send";
|
|
||||||
private static final String TAG_CAMERA = "camera";
|
|
||||||
|
|
||||||
private Recipient recipient;
|
|
||||||
private MediaSendViewModel viewModel;
|
|
||||||
|
|
||||||
private View countButton;
|
|
||||||
private TextView countButtonText;
|
|
||||||
private View cameraButton;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an intent to launch the media send flow starting with the picker.
|
|
||||||
*/
|
|
||||||
public static Intent buildGalleryIntent(@NonNull Context context, @NonNull Recipient recipient, @NonNull String body) {
|
|
||||||
Intent intent = new Intent(context, MediaSendActivity.class);
|
|
||||||
intent.putExtra(KEY_ADDRESS, recipient.getAddress().serialize());
|
|
||||||
intent.putExtra(KEY_BODY, body);
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an intent to launch the media send flow starting with the camera.
|
|
||||||
*/
|
|
||||||
public static Intent buildCameraIntent(@NonNull Context context, @NonNull Recipient recipient) {
|
|
||||||
Intent intent = buildGalleryIntent(context, recipient, "");
|
|
||||||
intent.putExtra(KEY_IS_CAMERA, true);
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an intent to launch the media send flow with a specific list of media. Will jump right to
|
|
||||||
* the editor screen.
|
|
||||||
*/
|
|
||||||
public static Intent buildEditorIntent(@NonNull Context context,
|
|
||||||
@NonNull List<Media> media,
|
|
||||||
@NonNull Recipient recipient,
|
|
||||||
@NonNull String body)
|
|
||||||
{
|
|
||||||
Intent intent = buildGalleryIntent(context, recipient, body);
|
|
||||||
intent.putParcelableArrayListExtra(KEY_MEDIA, new ArrayList<>(media));
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
|
||||||
super.onCreate(savedInstanceState, ready);
|
|
||||||
|
|
||||||
setContentView(R.layout.mediasend_activity);
|
|
||||||
setResult(RESULT_CANCELED);
|
|
||||||
|
|
||||||
if (savedInstanceState != null) { return; }
|
|
||||||
|
|
||||||
countButton = findViewById(R.id.mediasend_count_button);
|
|
||||||
countButtonText = findViewById(R.id.mediasend_count_button_text);
|
|
||||||
cameraButton = findViewById(R.id.mediasend_camera_button);
|
|
||||||
|
|
||||||
viewModel = new ViewModelProvider(this, new MediaSendViewModel.Factory(getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
|
||||||
recipient = Recipient.from(this, Address.fromSerialized(getIntent().getStringExtra(KEY_ADDRESS)), true);
|
|
||||||
|
|
||||||
viewModel.onBodyChanged(getIntent().getStringExtra(KEY_BODY));
|
|
||||||
|
|
||||||
List<Media> media = getIntent().getParcelableArrayListExtra(KEY_MEDIA);
|
|
||||||
boolean isCamera = getIntent().getBooleanExtra(KEY_IS_CAMERA, false);
|
|
||||||
|
|
||||||
if (isCamera) {
|
|
||||||
Fragment fragment = Camera1Fragment.newInstance();
|
|
||||||
getSupportFragmentManager().beginTransaction()
|
|
||||||
.replace(R.id.mediasend_fragment_container, fragment, TAG_CAMERA)
|
|
||||||
.commit();
|
|
||||||
|
|
||||||
} else if (!Util.isEmpty(media)) {
|
|
||||||
viewModel.onSelectedMediaChanged(this, media);
|
|
||||||
|
|
||||||
Fragment fragment = MediaSendFragment.newInstance(recipient);
|
|
||||||
getSupportFragmentManager().beginTransaction()
|
|
||||||
.replace(R.id.mediasend_fragment_container, fragment, TAG_SEND)
|
|
||||||
.commit();
|
|
||||||
} else {
|
|
||||||
MediaPickerFolderFragment fragment = MediaPickerFolderFragment.newInstance(recipient);
|
|
||||||
getSupportFragmentManager().beginTransaction()
|
|
||||||
.replace(R.id.mediasend_fragment_container, fragment, TAG_FOLDER_PICKER)
|
|
||||||
.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeCountButtonObserver();
|
|
||||||
initializeCameraButtonObserver();
|
|
||||||
initializeErrorObserver();
|
|
||||||
|
|
||||||
cameraButton.setOnClickListener(v -> {
|
|
||||||
int maxSelection = viewModel.getMaxSelection();
|
|
||||||
|
|
||||||
if (viewModel.getSelectedMedia().getValue() != null && viewModel.getSelectedMedia().getValue().size() >= maxSelection) {
|
|
||||||
Toast.makeText(this, getString(R.string.attachmentsErrorNumber), Toast.LENGTH_SHORT).show();
|
|
||||||
} else {
|
|
||||||
navigateToCamera();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
MediaSendFragment sendFragment = (MediaSendFragment) getSupportFragmentManager().findFragmentByTag(TAG_SEND);
|
|
||||||
if (sendFragment == null || !sendFragment.isVisible() || !sendFragment.handleBackPress()) {
|
|
||||||
super.onBackPressed();
|
|
||||||
|
|
||||||
if (getIntent().getBooleanExtra(KEY_IS_CAMERA, false) && getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
|
||||||
viewModel.onImageCaptureUndo(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
|
||||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFolderSelected(@NonNull MediaFolder folder) {
|
|
||||||
if (viewModel == null) { return; }
|
|
||||||
|
|
||||||
viewModel.onFolderSelected(folder.getBucketId());
|
|
||||||
|
|
||||||
MediaPickerItemFragment fragment = MediaPickerItemFragment.newInstance(folder.getBucketId(), folder.getTitle(), viewModel.getMaxSelection());
|
|
||||||
getSupportFragmentManager().beginTransaction()
|
|
||||||
.setCustomAnimations(R.anim.slide_from_right, R.anim.slide_to_left, R.anim.slide_from_left, R.anim.slide_to_right)
|
|
||||||
.replace(R.id.mediasend_fragment_container, fragment, TAG_ITEM_PICKER)
|
|
||||||
.addToBackStack(null)
|
|
||||||
.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMediaSelected(@NonNull Media media) {
|
|
||||||
viewModel.onSingleMediaSelected(this, media);
|
|
||||||
navigateToMediaSend(recipient);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAddMediaClicked(@NonNull String bucketId) {
|
|
||||||
MediaPickerFolderFragment folderFragment = MediaPickerFolderFragment.newInstance(recipient);
|
|
||||||
MediaPickerItemFragment itemFragment = MediaPickerItemFragment.newInstance(bucketId, "", viewModel.getMaxSelection());
|
|
||||||
|
|
||||||
getSupportFragmentManager().beginTransaction()
|
|
||||||
.setCustomAnimations(R.anim.stationary, R.anim.slide_to_left, R.anim.slide_from_left, R.anim.slide_to_right)
|
|
||||||
.replace(R.id.mediasend_fragment_container, folderFragment, TAG_FOLDER_PICKER)
|
|
||||||
.addToBackStack(null)
|
|
||||||
.commit();
|
|
||||||
|
|
||||||
getSupportFragmentManager().beginTransaction()
|
|
||||||
.setCustomAnimations(R.anim.slide_from_right, R.anim.stationary, R.anim.slide_from_left, R.anim.slide_to_right)
|
|
||||||
.replace(R.id.mediasend_fragment_container, itemFragment, TAG_ITEM_PICKER)
|
|
||||||
.addToBackStack(null)
|
|
||||||
.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSendClicked(@NonNull List<Media> media, @NonNull String message) {
|
|
||||||
viewModel.onSendClicked();
|
|
||||||
|
|
||||||
ArrayList<Media> mediaList = new ArrayList<>(media);
|
|
||||||
Intent intent = new Intent();
|
|
||||||
|
|
||||||
intent.putParcelableArrayListExtra(EXTRA_MEDIA, mediaList);
|
|
||||||
intent.putExtra(EXTRA_MESSAGE, message);
|
|
||||||
setResult(RESULT_OK, intent);
|
|
||||||
finish();
|
|
||||||
|
|
||||||
overridePendingTransition(R.anim.stationary, R.anim.camera_slide_to_bottom);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNoMediaAvailable() {
|
|
||||||
setResult(RESULT_CANCELED);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTouchEventsNeeded(boolean needed) {
|
|
||||||
MediaSendFragment fragment = (MediaSendFragment) getSupportFragmentManager().findFragmentByTag(TAG_SEND);
|
|
||||||
if (fragment != null) {
|
|
||||||
fragment.onTouchEventsNeeded(needed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCameraError() {
|
|
||||||
Toast.makeText(this, R.string.cameraErrorUnavailable, Toast.LENGTH_SHORT).show();
|
|
||||||
setResult(RESULT_CANCELED, new Intent());
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onImageCaptured(@NonNull byte[] data, int width, int height) {
|
|
||||||
Log.i(TAG, "Camera image captured.");
|
|
||||||
SimpleTask.run(getLifecycle(), () -> {
|
|
||||||
try {
|
|
||||||
Uri uri = BlobProvider.getInstance()
|
|
||||||
.forData(data)
|
|
||||||
.withMimeType(MediaTypes.IMAGE_JPEG)
|
|
||||||
.createForSingleSessionOnDisk(this, e -> Log.w(TAG, "Failed to write to disk.", e));
|
|
||||||
|
|
||||||
return new Media(uri,
|
|
||||||
FilenameUtils.constructPhotoFilename(this),
|
|
||||||
MediaTypes.IMAGE_JPEG,
|
|
||||||
System.currentTimeMillis(),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
data.length,
|
|
||||||
Optional.of(Media.ALL_MEDIA_BUCKET_ID),
|
|
||||||
Optional.absent()
|
|
||||||
);
|
|
||||||
} catch (IOException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}, media -> {
|
|
||||||
if (media == null) {
|
|
||||||
onNoMediaAvailable();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i(TAG, "Camera capture stored: " + media.getUri().toString());
|
|
||||||
|
|
||||||
viewModel.onImageCaptured(media);
|
|
||||||
navigateToMediaSend(recipient);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getDisplayRotation() {
|
|
||||||
return getWindowManager().getDefaultDisplay().getRotation();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeCountButtonObserver() {
|
|
||||||
viewModel.getCountButtonState().observe(this, buttonState -> {
|
|
||||||
if (buttonState == null) return;
|
|
||||||
|
|
||||||
countButtonText.setText(String.valueOf(buttonState.getCount()));
|
|
||||||
countButton.setEnabled(buttonState.isVisible());
|
|
||||||
animateButtonVisibility(countButton, countButton.getVisibility(), buttonState.isVisible() ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
if (buttonState.getCount() > 0) {
|
|
||||||
countButton.setOnClickListener(v -> navigateToMediaSend(recipient));
|
|
||||||
if (buttonState.isVisible()) {
|
|
||||||
animateButtonTextChange(countButton);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
countButton.setOnClickListener(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeCameraButtonObserver() {
|
|
||||||
viewModel.getCameraButtonVisibility().observe(this, visible -> {
|
|
||||||
if (visible == null) return;
|
|
||||||
animateButtonVisibility(cameraButton, cameraButton.getVisibility(), visible ? View.VISIBLE : View.GONE);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeErrorObserver() {
|
|
||||||
viewModel.getError().observe(this, error -> {
|
|
||||||
if (error == null) return;
|
|
||||||
|
|
||||||
switch (error) {
|
|
||||||
case ITEM_TOO_LARGE:
|
|
||||||
Toast.makeText(this, R.string.attachmentsErrorSize, Toast.LENGTH_LONG).show();
|
|
||||||
break;
|
|
||||||
case TOO_MANY_ITEMS:
|
|
||||||
// In modern session we'll say you can't sent more than 32 items, but if we ever want
|
|
||||||
// the exact count of how many items the user attempted to send it's: viewModel.getMaxSelection()
|
|
||||||
Toast.makeText(this, getString(R.string.attachmentsErrorNumber), Toast.LENGTH_SHORT).show();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void navigateToMediaSend(@NonNull Recipient recipient) {
|
|
||||||
MediaSendFragment fragment = MediaSendFragment.newInstance(recipient);
|
|
||||||
String backstackTag = null;
|
|
||||||
|
|
||||||
if (getSupportFragmentManager().findFragmentByTag(TAG_SEND) != null) {
|
|
||||||
getSupportFragmentManager().popBackStack(TAG_SEND, FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
|
||||||
backstackTag = TAG_SEND;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSupportFragmentManager().beginTransaction()
|
|
||||||
.setCustomAnimations(R.anim.slide_from_right, R.anim.slide_to_left, R.anim.slide_from_left, R.anim.slide_to_right)
|
|
||||||
.replace(R.id.mediasend_fragment_container, fragment, TAG_SEND)
|
|
||||||
.addToBackStack(backstackTag)
|
|
||||||
.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void navigateToCamera() {
|
|
||||||
|
|
||||||
Context c = getApplicationContext();
|
|
||||||
String permanentDenialTxt = Phrase.from(c, R.string.permissionsCameraDenied)
|
|
||||||
.put(APP_NAME_KEY, c.getString(R.string.app_name))
|
|
||||||
.format().toString();
|
|
||||||
String requireCameraPermissionsTxt = Phrase.from(c, R.string.cameraGrantAccessDescription)
|
|
||||||
.put(APP_NAME_KEY, c.getString(R.string.app_name))
|
|
||||||
.format().toString();
|
|
||||||
|
|
||||||
Permissions.with(this)
|
|
||||||
.request(Manifest.permission.CAMERA)
|
|
||||||
.withPermanentDenialDialog(permanentDenialTxt)
|
|
||||||
.onAllGranted(() -> {
|
|
||||||
Camera1Fragment fragment = getOrCreateCameraFragment();
|
|
||||||
getSupportFragmentManager().beginTransaction()
|
|
||||||
.setCustomAnimations(R.anim.slide_from_right, R.anim.slide_to_left, R.anim.slide_from_left, R.anim.slide_to_right)
|
|
||||||
.replace(R.id.mediasend_fragment_container, fragment, TAG_CAMERA)
|
|
||||||
.addToBackStack(null)
|
|
||||||
.commit();
|
|
||||||
})
|
|
||||||
.onAnyDenied(() -> Toast.makeText(MediaSendActivity.this, requireCameraPermissionsTxt, Toast.LENGTH_LONG).show())
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Camera1Fragment getOrCreateCameraFragment() {
|
|
||||||
Camera1Fragment fragment = (Camera1Fragment) getSupportFragmentManager().findFragmentByTag(TAG_CAMERA);
|
|
||||||
|
|
||||||
return fragment != null ? fragment
|
|
||||||
: Camera1Fragment.newInstance();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void animateButtonVisibility(@NonNull View button, int oldVisibility, int newVisibility) {
|
|
||||||
if (oldVisibility == newVisibility) return;
|
|
||||||
|
|
||||||
if (button.getAnimation() != null) {
|
|
||||||
button.clearAnimation();
|
|
||||||
button.setVisibility(newVisibility);
|
|
||||||
} else if (newVisibility == View.VISIBLE) {
|
|
||||||
button.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
Animation animation = new ScaleAnimation(0, 1, 0, 1, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
|
|
||||||
animation.setDuration(250);
|
|
||||||
animation.setInterpolator(new OvershootInterpolator());
|
|
||||||
button.startAnimation(animation);
|
|
||||||
} else {
|
|
||||||
Animation animation = new ScaleAnimation(1, 0, 1, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
|
|
||||||
animation.setDuration(150);
|
|
||||||
animation.setInterpolator(new AccelerateDecelerateInterpolator());
|
|
||||||
animation.setAnimationListener(new SimpleAnimationListener() {
|
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(Animation animation) {
|
|
||||||
button.clearAnimation();
|
|
||||||
button.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
button.startAnimation(animation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void animateButtonTextChange(@NonNull View button) {
|
|
||||||
if (button.getAnimation() != null) {
|
|
||||||
button.clearAnimation();
|
|
||||||
}
|
|
||||||
|
|
||||||
Animation grow = new ScaleAnimation(1f, 1.3f, 1f, 1.3f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
|
|
||||||
grow.setDuration(125);
|
|
||||||
grow.setInterpolator(new AccelerateInterpolator());
|
|
||||||
grow.setAnimationListener(new SimpleAnimationListener() {
|
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(Animation animation) {
|
|
||||||
Animation shrink = new ScaleAnimation(1.3f, 1f, 1.3f, 1f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
|
|
||||||
shrink.setDuration(125);
|
|
||||||
shrink.setInterpolator(new DecelerateInterpolator());
|
|
||||||
button.startAnimation(shrink);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
button.startAnimation(grow);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRequestFullScreen(boolean fullScreen) {
|
|
||||||
MediaSendFragment sendFragment = (MediaSendFragment) getSupportFragmentManager().findFragmentByTag(TAG_SEND);
|
|
||||||
if (sendFragment != null && sendFragment.isVisible()) {
|
|
||||||
sendFragment.onRequestFullScreen(fullScreen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,548 @@
|
|||||||
|
package org.thoughtcrime.securesms.mediasend
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
|
import android.view.animation.AccelerateInterpolator
|
||||||
|
import android.view.animation.Animation
|
||||||
|
import android.view.animation.DecelerateInterpolator
|
||||||
|
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 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
|
||||||
|
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
|
||||||
|
import org.session.libsession.utilities.Util.isEmpty
|
||||||
|
import org.session.libsession.utilities.concurrent.SimpleTask
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
|
import org.session.libsignal.utilities.guava.Optional
|
||||||
|
import org.thoughtcrime.securesms.ScreenLockActionBarActivity
|
||||||
|
import org.thoughtcrime.securesms.mediasend.MediaSendViewModel.CountButtonState
|
||||||
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
|
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||||
|
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment
|
||||||
|
import org.thoughtcrime.securesms.util.FilenameUtils.constructPhotoFilename
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encompasses the entire flow of sending media, starting from the selection process to the actual
|
||||||
|
* captioning and editing of the content.
|
||||||
|
*
|
||||||
|
* 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 val viewModel: MediaSendViewModel by viewModels()
|
||||||
|
|
||||||
|
private var countButton: View? = null
|
||||||
|
private var countButtonText: TextView? = null
|
||||||
|
private var cameraButton: View? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||||
|
super.onCreate(savedInstanceState, ready)
|
||||||
|
|
||||||
|
setContentView(R.layout.mediasend_activity)
|
||||||
|
setResult(RESULT_CANCELED)
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
countButton = findViewById(R.id.mediasend_count_button)
|
||||||
|
countButtonText = findViewById(R.id.mediasend_count_button_text)
|
||||||
|
cameraButton = findViewById(R.id.mediasend_camera_button)
|
||||||
|
|
||||||
|
recipient = Recipient.from(
|
||||||
|
this, fromSerialized(
|
||||||
|
intent.getStringExtra(KEY_ADDRESS)!!
|
||||||
|
), true
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModel.onBodyChanged(intent.getStringExtra(KEY_BODY)!!)
|
||||||
|
|
||||||
|
val media: List<Media?>? = intent.getParcelableArrayListExtra(KEY_MEDIA)
|
||||||
|
val isCamera = intent.getBooleanExtra(KEY_IS_CAMERA, false)
|
||||||
|
|
||||||
|
if (isCamera) {
|
||||||
|
val fragment: Fragment = Camera1Fragment.newInstance()
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.mediasend_fragment_container, fragment, TAG_CAMERA)
|
||||||
|
.commit()
|
||||||
|
} else if (!isEmpty(media)) {
|
||||||
|
viewModel.onSelectedMediaChanged(this, media!!)
|
||||||
|
|
||||||
|
val fragment: Fragment = MediaSendFragment.newInstance(recipient!!)
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.mediasend_fragment_container, fragment, TAG_SEND)
|
||||||
|
.commit()
|
||||||
|
} else {
|
||||||
|
val fragment = MediaPickerFolderFragment.newInstance(
|
||||||
|
recipient!!
|
||||||
|
)
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.mediasend_fragment_container, fragment, TAG_FOLDER_PICKER)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeCountButtonObserver()
|
||||||
|
initializeCameraButtonObserver()
|
||||||
|
initializeErrorObserver()
|
||||||
|
|
||||||
|
cameraButton?.setOnClickListener { v: View? ->
|
||||||
|
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 {
|
||||||
|
navigateToCamera()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
val sendFragment = supportFragmentManager.findFragmentByTag(TAG_SEND) as MediaSendFragment?
|
||||||
|
if (sendFragment == null || !sendFragment.isVisible || !sendFragment.handleBackPress()) {
|
||||||
|
super.onBackPressed()
|
||||||
|
|
||||||
|
if (intent.getBooleanExtra(
|
||||||
|
KEY_IS_CAMERA,
|
||||||
|
false
|
||||||
|
) && supportFragmentManager.backStackEntryCount == 0
|
||||||
|
) {
|
||||||
|
viewModel.onImageCaptureUndo(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(
|
||||||
|
requestCode: Int,
|
||||||
|
permissions: Array<String>,
|
||||||
|
grantResults: IntArray
|
||||||
|
) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFolderSelected(folder: MediaFolder) {
|
||||||
|
viewModel.onFolderSelected(folder.bucketId)
|
||||||
|
|
||||||
|
val fragment = MediaPickerItemFragment.newInstance(
|
||||||
|
folder.bucketId,
|
||||||
|
folder.title,
|
||||||
|
MediaSendViewModel.MAX_SELECTED_FILES
|
||||||
|
)
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.setCustomAnimations(
|
||||||
|
R.anim.slide_from_right,
|
||||||
|
R.anim.slide_to_left,
|
||||||
|
R.anim.slide_from_left,
|
||||||
|
R.anim.slide_to_right
|
||||||
|
)
|
||||||
|
.replace(R.id.mediasend_fragment_container, fragment, TAG_ITEM_PICKER)
|
||||||
|
.addToBackStack(null)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMediaSelected(media: Media) {
|
||||||
|
viewModel.onSingleMediaSelected(this, media)
|
||||||
|
navigateToMediaSend(recipient!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAddMediaClicked(bucketId: String) {
|
||||||
|
val folderFragment = MediaPickerFolderFragment.newInstance(
|
||||||
|
recipient!!
|
||||||
|
)
|
||||||
|
val itemFragment =
|
||||||
|
MediaPickerItemFragment.newInstance(bucketId, "", MediaSendViewModel.MAX_SELECTED_FILES)
|
||||||
|
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.setCustomAnimations(
|
||||||
|
R.anim.stationary,
|
||||||
|
R.anim.slide_to_left,
|
||||||
|
R.anim.slide_from_left,
|
||||||
|
R.anim.slide_to_right
|
||||||
|
)
|
||||||
|
.replace(R.id.mediasend_fragment_container, folderFragment, TAG_FOLDER_PICKER)
|
||||||
|
.addToBackStack(null)
|
||||||
|
.commit()
|
||||||
|
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.setCustomAnimations(
|
||||||
|
R.anim.slide_from_right,
|
||||||
|
R.anim.stationary,
|
||||||
|
R.anim.slide_from_left,
|
||||||
|
R.anim.slide_to_right
|
||||||
|
)
|
||||||
|
.replace(R.id.mediasend_fragment_container, itemFragment, TAG_ITEM_PICKER)
|
||||||
|
.addToBackStack(null)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSendClicked(media: List<Media>, message: String) {
|
||||||
|
viewModel.onSendClicked()
|
||||||
|
|
||||||
|
val mediaList = ArrayList(media)
|
||||||
|
val intent = Intent()
|
||||||
|
|
||||||
|
intent.putParcelableArrayListExtra(EXTRA_MEDIA, mediaList)
|
||||||
|
intent.putExtra(EXTRA_MESSAGE, message)
|
||||||
|
setResult(RESULT_OK, intent)
|
||||||
|
finish()
|
||||||
|
|
||||||
|
overridePendingTransition(R.anim.stationary, R.anim.camera_slide_to_bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNoMediaAvailable() {
|
||||||
|
setResult(RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTouchEventsNeeded(needed: Boolean) {
|
||||||
|
val fragment = supportFragmentManager.findFragmentByTag(TAG_SEND) as MediaSendFragment?
|
||||||
|
fragment?.onTouchEventsNeeded(needed)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCameraError() {
|
||||||
|
Toast.makeText(this, R.string.cameraErrorUnavailable, Toast.LENGTH_SHORT).show()
|
||||||
|
setResult(RESULT_CANCELED, Intent())
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onImageCaptured(data: ByteArray, width: Int, height: Int) {
|
||||||
|
Log.i(TAG, "Camera image captured.")
|
||||||
|
SimpleTask.run(lifecycle, {
|
||||||
|
try {
|
||||||
|
val uri = BlobProvider.getInstance()
|
||||||
|
.forData(data)
|
||||||
|
.withMimeType(MediaTypes.IMAGE_JPEG)
|
||||||
|
.createForSingleSessionOnDisk(
|
||||||
|
this
|
||||||
|
) { e: IOException? ->
|
||||||
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
"Failed to write to disk.",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return@run Media(
|
||||||
|
uri,
|
||||||
|
constructPhotoFilename(this),
|
||||||
|
MediaTypes.IMAGE_JPEG,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
data.size.toLong(),
|
||||||
|
Optional.of<String>(Media.ALL_MEDIA_BUCKET_ID),
|
||||||
|
Optional.absent<String>()
|
||||||
|
)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
return@run null
|
||||||
|
}
|
||||||
|
}, { media: Media? ->
|
||||||
|
if (media == null) {
|
||||||
|
onNoMediaAvailable()
|
||||||
|
return@run
|
||||||
|
}
|
||||||
|
Log.i(TAG, "Camera capture stored: " + media.uri.toString())
|
||||||
|
|
||||||
|
viewModel.onImageCaptured(media)
|
||||||
|
navigateToMediaSend(recipient!!)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDisplayRotation(): Int {
|
||||||
|
return windowManager.defaultDisplay.rotation
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeCountButtonObserver() {
|
||||||
|
viewModel.getCountButtonState().observe(
|
||||||
|
this
|
||||||
|
) { buttonState: CountButtonState? ->
|
||||||
|
if (buttonState == null) return@observe
|
||||||
|
countButtonText!!.text = buttonState.count.toString()
|
||||||
|
countButton!!.isEnabled = buttonState.isVisible
|
||||||
|
animateButtonVisibility(
|
||||||
|
countButton!!,
|
||||||
|
countButton!!.visibility,
|
||||||
|
if (buttonState.isVisible) View.VISIBLE else View.GONE
|
||||||
|
)
|
||||||
|
if (buttonState.count > 0) {
|
||||||
|
countButton!!.setOnClickListener { v: View? ->
|
||||||
|
navigateToMediaSend(
|
||||||
|
recipient!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (buttonState.isVisible) {
|
||||||
|
animateButtonTextChange(countButton!!)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
countButton!!.setOnClickListener(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeCameraButtonObserver() {
|
||||||
|
viewModel.getCameraButtonVisibility().observe(
|
||||||
|
this
|
||||||
|
) { visible: Boolean? ->
|
||||||
|
if (visible == null) return@observe
|
||||||
|
animateButtonVisibility(
|
||||||
|
cameraButton!!,
|
||||||
|
cameraButton!!.visibility,
|
||||||
|
if (visible) View.VISIBLE else View.GONE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeErrorObserver() {
|
||||||
|
viewModel.getError().observe(
|
||||||
|
this
|
||||||
|
) { error: MediaSendViewModel.Error? ->
|
||||||
|
if (error == null) return@observe
|
||||||
|
when (error) {
|
||||||
|
MediaSendViewModel.Error.ITEM_TOO_LARGE -> Toast.makeText(
|
||||||
|
this,
|
||||||
|
R.string.attachmentsErrorSize,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
|
||||||
|
MediaSendViewModel.Error.TOO_MANY_ITEMS -> // In modern session we'll say you can't sent more than 32 items, but if we ever want
|
||||||
|
// the exact count of how many items the user attempted to send it's: viewModel.getMaxSelection()
|
||||||
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
getString(R.string.attachmentsErrorNumber),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToMediaSend(recipient: Recipient) {
|
||||||
|
val fragment = MediaSendFragment.newInstance(recipient)
|
||||||
|
var backstackTag: String? = null
|
||||||
|
|
||||||
|
if (supportFragmentManager.findFragmentByTag(TAG_SEND) != null) {
|
||||||
|
supportFragmentManager.popBackStack(TAG_SEND, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||||
|
backstackTag = TAG_SEND
|
||||||
|
}
|
||||||
|
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.setCustomAnimations(
|
||||||
|
R.anim.slide_from_right,
|
||||||
|
R.anim.slide_to_left,
|
||||||
|
R.anim.slide_from_left,
|
||||||
|
R.anim.slide_to_right
|
||||||
|
)
|
||||||
|
.replace(R.id.mediasend_fragment_container, fragment, TAG_SEND)
|
||||||
|
.addToBackStack(backstackTag)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToCamera() {
|
||||||
|
val c = applicationContext
|
||||||
|
val permanentDenialTxt = Phrase.from(c, R.string.permissionsCameraDenied)
|
||||||
|
.put(APP_NAME_KEY, c.getString(R.string.app_name))
|
||||||
|
.format().toString()
|
||||||
|
val requireCameraPermissionsTxt = Phrase.from(c, R.string.cameraGrantAccessDescription)
|
||||||
|
.put(APP_NAME_KEY, c.getString(R.string.app_name))
|
||||||
|
.format().toString()
|
||||||
|
|
||||||
|
Permissions.with(this)
|
||||||
|
.request(Manifest.permission.CAMERA)
|
||||||
|
.withPermanentDenialDialog(permanentDenialTxt)
|
||||||
|
.onAllGranted {
|
||||||
|
val fragment = orCreateCameraFragment
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.setCustomAnimations(
|
||||||
|
R.anim.slide_from_right,
|
||||||
|
R.anim.slide_to_left,
|
||||||
|
R.anim.slide_from_left,
|
||||||
|
R.anim.slide_to_right
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
R.id.mediasend_fragment_container,
|
||||||
|
fragment,
|
||||||
|
TAG_CAMERA
|
||||||
|
)
|
||||||
|
.addToBackStack(null)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
.onAnyDenied {
|
||||||
|
Toast.makeText(
|
||||||
|
this@MediaSendActivity,
|
||||||
|
requireCameraPermissionsTxt,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
.execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val orCreateCameraFragment: Camera1Fragment
|
||||||
|
get() {
|
||||||
|
val fragment =
|
||||||
|
supportFragmentManager.findFragmentByTag(TAG_CAMERA) as Camera1Fragment?
|
||||||
|
|
||||||
|
return fragment ?: Camera1Fragment.newInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateButtonVisibility(button: View, oldVisibility: Int, newVisibility: Int) {
|
||||||
|
if (oldVisibility == newVisibility) return
|
||||||
|
|
||||||
|
if (button.animation != null) {
|
||||||
|
button.clearAnimation()
|
||||||
|
button.visibility = newVisibility
|
||||||
|
} else if (newVisibility == View.VISIBLE) {
|
||||||
|
button.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
val animation: Animation = ScaleAnimation(
|
||||||
|
0f,
|
||||||
|
1f,
|
||||||
|
0f,
|
||||||
|
1f,
|
||||||
|
Animation.RELATIVE_TO_SELF,
|
||||||
|
0.5f,
|
||||||
|
Animation.RELATIVE_TO_SELF,
|
||||||
|
0.5f
|
||||||
|
)
|
||||||
|
animation.duration = 250
|
||||||
|
animation.interpolator = OvershootInterpolator()
|
||||||
|
button.startAnimation(animation)
|
||||||
|
} else {
|
||||||
|
val animation: Animation = ScaleAnimation(
|
||||||
|
1f,
|
||||||
|
0f,
|
||||||
|
1f,
|
||||||
|
0f,
|
||||||
|
Animation.RELATIVE_TO_SELF,
|
||||||
|
0.5f,
|
||||||
|
Animation.RELATIVE_TO_SELF,
|
||||||
|
0.5f
|
||||||
|
)
|
||||||
|
animation.duration = 150
|
||||||
|
animation.interpolator = AccelerateDecelerateInterpolator()
|
||||||
|
animation.setAnimationListener(object : SimpleAnimationListener() {
|
||||||
|
override fun onAnimationEnd(animation: Animation) {
|
||||||
|
button.clearAnimation()
|
||||||
|
button.visibility = View.GONE
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
button.startAnimation(animation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateButtonTextChange(button: View) {
|
||||||
|
if (button.animation != null) {
|
||||||
|
button.clearAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
val grow: Animation = ScaleAnimation(
|
||||||
|
1f,
|
||||||
|
1.3f,
|
||||||
|
1f,
|
||||||
|
1.3f,
|
||||||
|
Animation.RELATIVE_TO_SELF,
|
||||||
|
0.5f,
|
||||||
|
Animation.RELATIVE_TO_SELF,
|
||||||
|
0.5f
|
||||||
|
)
|
||||||
|
grow.duration = 125
|
||||||
|
grow.interpolator = AccelerateInterpolator()
|
||||||
|
grow.setAnimationListener(object : SimpleAnimationListener() {
|
||||||
|
override fun onAnimationEnd(animation: Animation) {
|
||||||
|
val shrink: Animation = ScaleAnimation(
|
||||||
|
1.3f,
|
||||||
|
1f,
|
||||||
|
1.3f,
|
||||||
|
1f,
|
||||||
|
Animation.RELATIVE_TO_SELF,
|
||||||
|
0.5f,
|
||||||
|
Animation.RELATIVE_TO_SELF,
|
||||||
|
0.5f
|
||||||
|
)
|
||||||
|
shrink.duration = 125
|
||||||
|
shrink.interpolator = DecelerateInterpolator()
|
||||||
|
button.startAnimation(shrink)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
button.startAnimation(grow)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestFullScreen(fullScreen: Boolean) {
|
||||||
|
val sendFragment = supportFragmentManager.findFragmentByTag(TAG_SEND) as MediaSendFragment?
|
||||||
|
if (sendFragment != null && sendFragment.isVisible) {
|
||||||
|
sendFragment.onRequestFullScreen(fullScreen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG: String = MediaSendActivity::class.java.simpleName
|
||||||
|
|
||||||
|
const val EXTRA_MEDIA: String = "media"
|
||||||
|
const val EXTRA_MESSAGE: String = "message"
|
||||||
|
|
||||||
|
private const val KEY_ADDRESS = "address"
|
||||||
|
private const val KEY_BODY = "body"
|
||||||
|
private const val KEY_MEDIA = "media"
|
||||||
|
private const val KEY_IS_CAMERA = "is_camera"
|
||||||
|
|
||||||
|
private const val TAG_FOLDER_PICKER = "folder_picker"
|
||||||
|
private const val TAG_ITEM_PICKER = "item_picker"
|
||||||
|
private const val TAG_SEND = "send"
|
||||||
|
private const val TAG_CAMERA = "camera"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an intent to launch the media send flow starting with the picker.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun buildGalleryIntent(context: Context, recipient: Recipient, body: String): Intent {
|
||||||
|
val intent = Intent(context, MediaSendActivity::class.java)
|
||||||
|
intent.putExtra(KEY_ADDRESS, recipient.address.serialize())
|
||||||
|
intent.putExtra(KEY_BODY, body)
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an intent to launch the media send flow starting with the camera.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun buildCameraIntent(context: Context, recipient: Recipient): Intent {
|
||||||
|
val intent = buildGalleryIntent(context, recipient, "")
|
||||||
|
intent.putExtra(KEY_IS_CAMERA, true)
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an intent to launch the media send flow with a specific list of media. Will jump right to
|
||||||
|
* the editor screen.
|
||||||
|
*/
|
||||||
|
fun buildEditorIntent(
|
||||||
|
context: Context,
|
||||||
|
media: List<Media>,
|
||||||
|
recipient: Recipient,
|
||||||
|
body: String
|
||||||
|
): Intent {
|
||||||
|
val intent = buildGalleryIntent(context, recipient, body)
|
||||||
|
intent.putParcelableArrayListExtra(KEY_MEDIA, ArrayList(media))
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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…
Reference in New Issue