Merge branch 'dev' of github.com:session-foundation/session-android into dev

pull/1710/head
alansley 2 months ago
commit f83db366b0

@ -34,6 +34,7 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.request.transition.Transition;
import dagger.hilt.android.AndroidEntryPoint;
import network.loki.messenger.R;
import org.session.libsignal.utilities.Log;
import com.bumptech.glide.Glide;
@ -43,6 +44,7 @@ import org.session.libsession.utilities.TextSecurePreferences;
import java.io.ByteArrayOutputStream;
@AndroidEntryPoint
public class Camera1Fragment extends Fragment implements TextureView.SurfaceTextureListener,
Camera1Controller.EventListener
{
@ -80,7 +82,7 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
controller = (Controller) getActivity();
camera = new Camera1Controller(TextSecurePreferences.getDirectCaptureCameraId(getContext()), displaySize.x, displaySize.y, this);
orderEnforcer = new OrderEnforcer<>(Stage.SURFACE_AVAILABLE, Stage.CAMERA_PROPERTIES_AVAILABLE);
viewModel = new ViewModelProvider(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
viewModel = new ViewModelProvider(requireActivity()).get(MediaSendViewModel.class);
}
@Nullable

@ -28,11 +28,13 @@ import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.guava.Optional;
import dagger.hilt.android.AndroidEntryPoint;
import network.loki.messenger.R;
/**
* Allows the user to select a media folder to explore.
*/
@AndroidEntryPoint
public class MediaPickerFolderFragment extends Fragment implements MediaPickerFolderAdapter.EventListener {
private static final String KEY_RECIPIENT_NAME = "recipient_name";
@ -60,7 +62,7 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
recipientName = getArguments().getString(KEY_RECIPIENT_NAME);
viewModel = new ViewModelProvider(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
viewModel = new ViewModelProvider(requireActivity()).get(MediaSendViewModel.class);
}
@Override

@ -27,11 +27,13 @@ import org.session.libsession.utilities.Util;
import java.util.ArrayList;
import java.util.List;
import dagger.hilt.android.AndroidEntryPoint;
import network.loki.messenger.R;
/**
* Allows the user to select a set of media items from a specified folder.
*/
@AndroidEntryPoint
public class MediaPickerItemFragment extends Fragment implements MediaPickerItemAdapter.EventListener {
private static final String KEY_BUCKET_ID = "bucket_id";
@ -66,7 +68,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
bucketId = getArguments().getString(KEY_BUCKET_ID);
folderTitle = getArguments().getString(KEY_FOLDER_TITLE);
maxSelection = getArguments().getInt(KEY_MAX_SELECTION);
viewModel = new ViewModelProvider(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
viewModel = new ViewModelProvider(requireActivity()).get(MediaSendViewModel.class);
}
@Override

@ -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
}
}
}

@ -36,6 +36,8 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import dagger.hilt.android.AndroidEntryPoint;
import network.loki.messenger.R;
import org.session.libsession.utilities.MediaTypes;
import org.session.libsession.utilities.TextSecurePreferences;
@ -59,6 +61,7 @@ import org.thoughtcrime.securesms.util.Stopwatch;
/**
* Allows the user to edit and caption a set of media items before choosing to send them.
*/
@AndroidEntryPoint
public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGlobalLayoutListener,
MediaRailAdapter.RailItemListener,
InputAwareLayout.OnKeyboardShownListener,
@ -108,6 +111,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
}
controller = (Controller) requireActivity();
viewModel = new ViewModelProvider(requireActivity()).get(MediaSendViewModel.class);
}
@Override
@ -264,8 +268,6 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
}
private void initViewModel() {
viewModel = new ViewModelProvider(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
viewModel.getSelectedMedia().observe(this, media -> {
if (Util.isEmpty(media)) {
controller.onNoMediaAvailable();

@ -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…
Cancel
Save