Merge camera into send flow.

This commit is contained in:
Greyson Parrelli 2019-03-13 16:05:25 -07:00
parent eb1dd58a0b
commit 0a8bbf14a6
18 changed files with 267 additions and 246 deletions

View File

@ -439,11 +439,6 @@
android:exported="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".camera.CameraActivity"
android:theme="@style/TextSecure.ScribbleTheme"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>

View File

@ -3,7 +3,7 @@
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator">
<translate
android:duration="150"
android:duration="250"
android:fromXDelta="-100%"
android:toXDelta="0%" />
</set>

View File

@ -3,7 +3,7 @@
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator">
<translate
android:duration="150"
android:duration="250"
android:fromXDelta="100%"
android:toXDelta="0%" />
</set>

View File

@ -3,7 +3,7 @@
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator">
<translate
android:duration="150"
android:duration="250"
android:fromXDelta="0%"
android:toXDelta="-100%" />
</set>

View File

@ -3,7 +3,7 @@
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator">
<translate
android:duration="150"
android:duration="250"
android:fromXDelta="0%"
android:toXDelta="100%" />
</set>

View File

@ -20,8 +20,8 @@
android:id="@+id/camera_flip_button"
android:layout_width="48dp"
android:layout_height="wrap_content"
android:layout_below="@+id/camera_capture_button"
android:layout_marginTop="40dp"
android:layout_above="@+id/camera_capture_button"
android:layout_marginBottom="40dp"
android:layout_centerHorizontal="true"
android:src="@drawable/ic_camera_front"
android:scaleType="fitCenter"

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:background="@color/core_black">
<TextureView
android:id="@+id/camera_preview"

View File

@ -83,7 +83,6 @@
<string name="CallScreen_Incoming_call">Incoming call</string>
<!-- CameraActivity -->
<string name="CameraActivity_camera_unavailable">Camera unavailable.</string>
<string name="CameraActivity_image_save_failure">Failed to save image.</string>
<!-- ClearProfileActivity -->
@ -467,6 +466,7 @@
<!-- MediaSendActivity -->
<string name="MediaSendActivity_add_a_caption">Add a caption...</string>
<string name="MediaSendActivity_an_item_was_removed_because_it_exceeded_the_size_limit">An item was removed because it exceeded the size limit</string>
<string name="MediaSendActivity_camera_unavailable">Camera unavailable.</string>
<!-- MediaRepository -->
<string name="MediaRepository_all_media">All media</string>

View File

@ -109,6 +109,9 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
camera.setScreenRotation(controller.getDisplayRotation());
});
orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, this::updatePreviewScale);
requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
}
@Override
@ -156,6 +159,10 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
controller.onCameraError();
}
public void reset() {
orderEnforcer.reset();
}
@SuppressLint("ClickableViewAccessibility")
private void initControls() {
flipButton = getView().findViewById(R.id.camera_flip_button);
@ -202,7 +209,7 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
}
private void onCaptureClicked() {
orderEnforcer.reset();
reset();
Stopwatch fastCaptureTimer = new Stopwatch("Capture");
@ -230,7 +237,7 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
fastCaptureTimer.split("bytes");
fastCaptureTimer.stop(TAG);
controller.onImageCaptured(data);
controller.onImageCaptured(data, resource.getWidth(), resource.getHeight());
}
@Override
@ -299,7 +306,7 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
public interface Controller {
void onCameraError();
void onImageCaptured(@NonNull byte[] data);
void onImageCaptured(@NonNull byte[] data, int width, int height);
int getDisplayRotation();
}

View File

@ -1,166 +0,0 @@
package org.thoughtcrime.securesms.camera;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.widget.ImageView;
import android.widget.Toast;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.scribbles.ScribbleFragment;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.whispersystems.libsignal.util.guava.Optional;
public class CameraActivity extends PassphraseRequiredActionBarActivity implements Camera1Fragment.Controller,
ScribbleFragment.Controller
{
private static final String TAG = CameraActivity.class.getSimpleName();
private static final String TAG_CAMERA = "camera";
private static final String TAG_EDITOR = "editor";
private static final String KEY_TRANSPORT = "transport";
public static final String EXTRA_MESSAGE = "message";
public static final String EXTRA_TRANSPORT = "transport";
public static final String EXTRA_WIDTH = "width";
public static final String EXTRA_HEIGHT = "height";
public static final String EXTRA_SIZE = "size";
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
private ImageView snapshot;
private TransportOption transport;
private Uri captureUri;
private boolean imageSent;
public static Intent getIntent(@NonNull Context context, @NonNull TransportOption transport) {
Intent intent = new Intent(context, CameraActivity.class);
intent.putExtra(KEY_TRANSPORT, transport);
return intent;
}
@Override
protected void onPreCreate() {
dynamicLanguage.onCreate(this);
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState, boolean ready) {
setContentView(R.layout.camera_activity);
snapshot = findViewById(R.id.camera_snapshot);
transport = getIntent().getParcelableExtra(KEY_TRANSPORT);
if (savedInstanceState == null) {
Camera1Fragment fragment = Camera1Fragment.newInstance();
getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, fragment, TAG_CAMERA).commit();
}
}
@Override
protected void onResume() {
super.onResume();
dynamicLanguage.onResume(this);
}
@Override
public void onBackPressed() {
ScribbleFragment editorFragment = (ScribbleFragment) getSupportFragmentManager().findFragmentByTag(TAG_EDITOR);
if (editorFragment != null && editorFragment.isEmojiKeyboardVisible()) {
editorFragment.dismissEmojiKeyboard();
} else {
if (editorFragment != null && captureUri != null) {
Log.i(TAG, "Cleaning up unused capture: " + captureUri);
BlobProvider.getInstance().delete(this, captureUri);
captureUri = null;
}
super.onBackPressed();
overridePendingTransition(R.anim.stationary, R.anim.camera_slide_to_bottom);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (captureUri != null) {
Log.i(TAG, "Cleaning up capture in onDestroy: " + captureUri);
BlobProvider.getInstance().delete(this, captureUri);
}
}
@Override
public void onCameraError() {
Toast.makeText(this, R.string.CameraActivity_camera_unavailable, Toast.LENGTH_SHORT).show();
setResult(RESULT_CANCELED, new Intent());
finish();
}
@Override
public void onImageCaptured(@NonNull byte[] data) {
Log.i(TAG, "Fast image captured.");
captureUri = BlobProvider.getInstance().forData(data).createForSingleSessionInMemory();
Log.i(TAG, "Fast image stored: " + captureUri.toString());
SettableFuture<Boolean> result = new SettableFuture<>();
GlideApp.with(this).load(new DecryptableStreamUriLoader.DecryptableUri(captureUri)).into(new GlideDrawableListeningTarget(snapshot, result));
result.addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
ScribbleFragment fragment = ScribbleFragment.newInstance(captureUri, dynamicLanguage.getCurrentLocale(), Optional.of(transport), true);
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
.replace(R.id.fragment_container, fragment, TAG_EDITOR)
.addToBackStack(null)
.commit();
}
});
}
@Override
public int getDisplayRotation() {
return getWindowManager().getDefaultDisplay().getRotation();
}
@Override
public void onImageEditComplete(@NonNull Uri uri, int width, int height, long size, @NonNull Optional<String> message, @NonNull Optional<TransportOption> transport) {
imageSent = true;
Intent intent = new Intent();
intent.setData(uri);
intent.putExtra(EXTRA_WIDTH, width);
intent.putExtra(EXTRA_HEIGHT, height);
intent.putExtra(EXTRA_SIZE, size);
intent.putExtra(EXTRA_MESSAGE, message.or(""));
intent.putExtra(EXTRA_TRANSPORT, transport.orNull());
setResult(RESULT_OK, intent);
finish();
overridePendingTransition(R.anim.stationary, R.anim.camera_slide_to_bottom);
}
@Override
public void onImageEditFailure() {
Log.w(TAG, "Failed to save edited image.");
Toast.makeText(this, R.string.CameraActivity_image_save_failure, Toast.LENGTH_SHORT).show();
finish();
}
@Override
public void onTouchEventsNeeded(boolean needed) { }
}

View File

@ -96,7 +96,6 @@ import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.VerifyIdentityActivity;
import org.thoughtcrime.securesms.audio.AudioRecorder;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.camera.CameraActivity;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.components.AnimatingToggle;
import org.thoughtcrime.securesms.components.AttachmentTypeSelector;
@ -269,8 +268,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private static final int PICK_LOCATION = 9;
private static final int PICK_GIF = 10;
private static final int SMS_DEFAULT = 11;
private static final int PICK_CAMERA = 12;
private static final int MEDIA_SENDER = 13;
private static final int MEDIA_SENDER = 12;
private GlideRequests glideRequests;
protected ComposeText composeText;
@ -533,35 +531,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
case SMS_DEFAULT:
initializeSecurity(isSecureText, isDefaultSms);
break;
case PICK_CAMERA:
int imageWidth = data.getIntExtra(CameraActivity.EXTRA_WIDTH, 0);
int imageHeight = data.getIntExtra(CameraActivity.EXTRA_HEIGHT, 0);
long imageSize = data.getLongExtra(CameraActivity.EXTRA_SIZE, 0);
TransportOption transport = data.getParcelableExtra(CameraActivity.EXTRA_TRANSPORT);
String message = data.getStringExtra(CameraActivity.EXTRA_MESSAGE);
SlideDeck slideDeck = new SlideDeck();
case MEDIA_SENDER:
long expiresIn = recipient.getExpireMessages() * 1000L;
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
boolean initiating = threadId == -1;
if (transport == null) {
throw new IllegalStateException("Received a null transport from the CameraActivity.");
}
sendButton.setTransport(transport);
slideDeck.addSlide(new ImageSlide(this, data.getData(), imageSize, imageWidth, imageHeight));
sendMediaMessage(transport.isSms(), message, slideDeck, Collections.emptyList(), Collections.emptyList(), expiresIn, subscriptionId, initiating);
break;
case MEDIA_SENDER:
expiresIn = recipient.getExpireMessages() * 1000L;
subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
initiating = threadId == -1;
transport = data.getParcelableExtra(MediaSendActivity.EXTRA_TRANSPORT);
message = data.getStringExtra(MediaSendActivity.EXTRA_MESSAGE);
slideDeck = new SlideDeck();
TransportOption transport = data.getParcelableExtra(MediaSendActivity.EXTRA_TRANSPORT);
String message = data.getStringExtra(MediaSendActivity.EXTRA_MESSAGE);
SlideDeck slideDeck = new SlideDeck();
if (transport == null) {
throw new IllegalStateException("Received a null transport from the MediaSendActivity.");
@ -592,6 +568,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
Stream.of(slideDeck.getSlides())
.map(Slide::getUri)
.withoutNulls()
.filter(BlobProvider::isAuthority)
.forEach(uri -> BlobProvider.getInstance().delete(context, uri));
});
}
@ -1231,7 +1208,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
final List<Media> mediaList = getIntent().getParcelableArrayListExtra(MEDIA_EXTRA);
if (!Util.isEmpty(mediaList)) {
Intent sendIntent = MediaSendActivity.getIntent(this, mediaList, recipient, draftText, sendButton.getSelectedTransport());
Intent sendIntent = MediaSendActivity.buildEditorIntent(this, mediaList, recipient, draftText, sendButton.getSelectedTransport());
startActivityForResult(sendIntent, MEDIA_SENDER);
return new SettableFuture<>(false);
}
@ -1743,7 +1720,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return new SettableFuture<>(false);
} else if (MediaType.IMAGE.equals(mediaType) || MediaType.GIF.equals(mediaType) || MediaType.VIDEO.equals(mediaType)) {
Media media = new Media(uri, MediaUtil.getMimeType(this, uri), 0, width, height, 0, Optional.absent(), Optional.absent());
startActivityForResult(MediaSendActivity.getIntent(ConversationActivity.this, Collections.singletonList(media), recipient, composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER);
startActivityForResult(MediaSendActivity.buildEditorIntent(ConversationActivity.this, Collections.singletonList(media), recipient, composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER);
return new SettableFuture<>(false);
} else {
return attachmentManager.setMedia(glideRequests, uri, mediaType, getCurrentMediaConstraints(), width, height);
@ -2392,7 +2369,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
linkPreviewViewModel.onUserCancel();
// TODO: Carry over size?
Media media = new Media(uri, mimeType, dateTaken, width, height, 0, Optional.of(Media.ALL_MEDIA_BUCKET_ID), Optional.absent());
startActivityForResult(MediaSendActivity.getIntent(ConversationActivity.this, Collections.singletonList(media), recipient, composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER);
startActivityForResult(MediaSendActivity.buildEditorIntent(ConversationActivity.this, Collections.singletonList(media), recipient, composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER);
}
}
@ -2406,7 +2383,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video))
.onAllGranted(() -> {
composeText.clearFocus();
startActivityForResult(CameraActivity.getIntent(ConversationActivity.this, sendButton.getSelectedTransport()), PICK_CAMERA);
startActivityForResult(MediaSendActivity.buildCameraIntent(ConversationActivity.this, recipient, sendButton.getSelectedTransport()), MEDIA_SENDER);
overridePendingTransition(R.anim.camera_slide_from_bottom, R.anim.stationary);
})
.onAnyDenied(() -> Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show())

View File

@ -52,7 +52,7 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
recipientName = getArguments().getString(KEY_RECIPIENT_NAME);
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(new MediaRepository())).get(MediaSendViewModel.class);
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
}
@Override

View File

@ -71,7 +71,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 = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(new MediaRepository())).get(MediaSendViewModel.class);
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
}
@Override

View File

@ -8,27 +8,34 @@ import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.view.View;
import android.widget.Button;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.OvershootInterpolator;
import android.view.animation.ScaleAnimation;
import android.widget.TextView;
import android.widget.Toast;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.camera.Camera1Fragment;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.scribbles.ScribbleFragment;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
@ -42,8 +49,11 @@ import java.util.Locale;
public class MediaSendActivity extends PassphraseRequiredActionBarActivity implements MediaPickerFolderFragment.Controller,
MediaPickerItemFragment.Controller,
MediaSendFragment.Controller,
ScribbleFragment.Controller
ScribbleFragment.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";
public static final String EXTRA_TRANSPORT = "transport";
@ -55,10 +65,12 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
private static final String KEY_BODY = "body";
private static final String KEY_MEDIA = "media";
private static final String KEY_TRANSPORT = "transport";
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 final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
@ -73,7 +85,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
/**
* Get an intent to launch the media send flow starting with the picker.
*/
public static Intent getIntent(@NonNull Context context, @NonNull Recipient recipient, @NonNull String body, @NonNull TransportOption transport) {
public static Intent buildGalleryIntent(@NonNull Context context, @NonNull Recipient recipient, @NonNull String body, @NonNull TransportOption transport) {
Intent intent = new Intent(context, MediaSendActivity.class);
intent.putExtra(KEY_ADDRESS, recipient.getAddress().serialize());
intent.putExtra(KEY_TRANSPORT, transport);
@ -81,17 +93,26 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
return intent;
}
/**
* Get an intent to launch the media send flow starting with the picker.
*/
public static Intent buildCameraIntent(@NonNull Context context, @NonNull Recipient recipient, @NonNull TransportOption transport) {
Intent intent = buildGalleryIntent(context, recipient, "", transport);
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 getIntent(@NonNull Context context,
@NonNull List<Media> media,
@NonNull Recipient recipient,
@NonNull String body,
@NonNull TransportOption transport)
public static Intent buildEditorIntent(@NonNull Context context,
@NonNull List<Media> media,
@NonNull Recipient recipient,
@NonNull String body,
@NonNull TransportOption transport)
{
Intent intent = getIntent(context, recipient, body, transport);
Intent intent = buildGalleryIntent(context, recipient, body, transport);
intent.putParcelableArrayListExtra(KEY_MEDIA, new ArrayList<>(media));
return intent;
}
@ -114,7 +135,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
countButton = findViewById(R.id.mediasend_count_button);
countButtonText = findViewById(R.id.mediasend_count_button_text);
viewModel = ViewModelProviders.of(this, new MediaSendViewModel.Factory(new MediaRepository())).get(MediaSendViewModel.class);
viewModel = ViewModelProviders.of(this, new MediaSendViewModel.Factory(getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
recipient = Recipient.from(this, Address.fromSerialized(getIntent().getStringExtra(KEY_ADDRESS)), true);
transport = getIntent().getParcelableExtra(KEY_TRANSPORT);
@ -123,9 +144,16 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
viewModel.onBodyChanged(getIntent().getStringExtra(KEY_BODY));
List<Media> media = getIntent().getParcelableArrayListExtra(KEY_MEDIA);
List<Media> media = getIntent().getParcelableArrayListExtra(KEY_MEDIA);
boolean isCamera = getIntent().getBooleanExtra(KEY_IS_CAMERA, false);
if (!Util.isEmpty(media)) {
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(transport, dynamicLanguage.getCurrentLocale());
@ -154,6 +182,10 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
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);
}
}
}
@ -195,6 +227,8 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
@Override
public void onSendClicked(@NonNull List<Media> media, @NonNull String message, @NonNull TransportOption transport) {
viewModel.onSendClicked();
ArrayList<Media> mediaList = new ArrayList<>(media);
Intent intent = new Intent();
@ -231,13 +265,72 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
}
}
@Override
public void onCameraError() {
Toast.makeText(this, R.string.MediaSendActivity_camera_unavailable, 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(MediaUtil.IMAGE_JPEG)
.createForSingleSessionOnDisk(this);
return new Media(uri,
MediaUtil.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) {
onImageEditFailure();
return;
}
Log.i(TAG, "Camera capture stored: " + media.getUri().toString());
viewModel.onImageCaptured(media);
navigateToMediaSend(transport, dynamicLanguage.getCurrentLocale());
});
}
@Override
public int getDisplayRotation() {
return getWindowManager().getDefaultDisplay().getRotation();
}
private void initializeCountButtonObserver(@NonNull TransportOption transport, @NonNull Locale locale) {
viewModel.getCountButtonState().observe(this, buttonState -> {
if (buttonState == null) return;
countButton.setVisibility(buttonState.getVisibility() ? View.VISIBLE : View.GONE);
countButton.setOnClickListener(v -> navigateToMediaSend(transport, locale));
countButtonText.setText(String.valueOf(buttonState.getCount()));
countButton.setEnabled(buttonState.getVisibility());
animateCountButtonVisibility(countButton, countButton.getVisibility(), buttonState.getVisibility() ? View.VISIBLE : View.GONE);
if (buttonState.getCount() > 0) {
countButton.setOnClickListener(v -> {
Camera1Fragment fragment = (Camera1Fragment) getSupportFragmentManager().findFragmentByTag(TAG_CAMERA);
if (fragment != null) {
fragment.reset();
}
navigateToMediaSend(transport, locale);
});
} else {
countButton.setOnClickListener(null);
}
});
}
@ -256,4 +349,33 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
.addToBackStack(backstackTag)
.commit();
}
private void animateCountButtonVisibility(View countButton, int oldVisibility, int newVisibility) {
if (oldVisibility == newVisibility) return;
if (countButton.getAnimation() != null) {
countButton.getAnimation().cancel();
countButton.setVisibility(newVisibility);
} else if (newVisibility == View.VISIBLE) {
countButton.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());
countButton.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) {
countButton.setVisibility(View.GONE);
}
});
countButton.startAnimation(animation);
}
}
}

View File

@ -259,7 +259,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
@Override
public void onRailItemDeleteClicked(int distanceFromActive) {
viewModel.onMediaItemRemoved(fragmentPager.getCurrentItem() + distanceFromActive);
viewModel.onMediaItemRemoved(requireContext(), fragmentPager.getCurrentItem() + distanceFromActive);
}
@Override
@ -296,7 +296,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
}
private void initViewModel() {
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(new MediaRepository())).get(MediaSendViewModel.class);
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
viewModel.getSelectedMedia().observe(this, media -> {
if (Util.isEmpty(media)) {

View File

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.mediasend;
import android.app.Application;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.ViewModel;
@ -12,12 +13,15 @@ import android.text.TextUtils;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@ -26,6 +30,7 @@ import java.util.Map;
*/
class MediaSendViewModel extends ViewModel {
private final Application application;
private final MediaRepository repository;
private final MutableLiveData<List<Media>> selectedMedia;
private final MutableLiveData<List<Media>> bucketMedia;
@ -39,8 +44,11 @@ class MediaSendViewModel extends ViewModel {
private MediaConstraints mediaConstraints;
private CharSequence body;
private CountButtonState.Visibility countButtonVisibility;
private boolean sentMedia;
private Optional<Media> lastImageCapture;
private MediaSendViewModel(@NonNull MediaRepository repository) {
private MediaSendViewModel(@NonNull Application application, @NonNull MediaRepository repository) {
this.application = application;
this.repository = repository;
this.selectedMedia = new MutableLiveData<>();
this.bucketMedia = new MutableLiveData<>();
@ -51,6 +59,7 @@ class MediaSendViewModel extends ViewModel {
this.error = new SingleLiveEvent<>();
this.savedDrawState = new HashMap<>();
this.countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
this.lastImageCapture = Optional.absent();
position.setValue(-1);
countButtonState.setValue(new CountButtonState(0, CountButtonState.Visibility.CONDITIONAL));
@ -91,17 +100,17 @@ class MediaSendViewModel extends ViewModel {
void onMultiSelectStarted() {
countButtonVisibility = CountButtonState.Visibility.FORCED_ON;
countButtonState.postValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
}
void onImageEditorStarted() {
countButtonVisibility = CountButtonState.Visibility.FORCED_OFF;
countButtonState.postValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
}
void onImageEditorEnded() {
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
countButtonState.postValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
}
void onBodyChanged(@NonNull CharSequence body) {
@ -117,11 +126,51 @@ class MediaSendViewModel extends ViewModel {
this.position.setValue(position);
}
void onMediaItemRemoved(int position) {
getSelectedMediaOrDefault().remove(position);
void onMediaItemRemoved(@NonNull Context context, int position) {
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<>();
}
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 onCaptionChanged(@NonNull String newCaption) {
if (position.getValue() >= 0 && !Util.isEmpty(selectedMedia.getValue())) {
selectedMedia.getValue().get(position.getValue()).setCaption(TextUtils.isEmpty(newCaption) ? null : newCaption);
@ -133,6 +182,10 @@ class MediaSendViewModel extends ViewModel {
savedDrawState.putAll(state);
}
void onSendClicked() {
sentMedia = true;
}
@NonNull Map<Uri, Object> getDrawState() {
return savedDrawState;
}
@ -188,6 +241,16 @@ class MediaSendViewModel extends ViewModel {
}
@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
}
@ -221,15 +284,17 @@ class MediaSendViewModel extends ViewModel {
static class Factory extends ViewModelProvider.NewInstanceFactory {
private final Application application;
private final MediaRepository repository;
Factory(@NonNull MediaRepository repository) {
this.repository = 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(repository));
return modelClass.cast(new MediaSendViewModel(application, repository));
}
}
}

View File

@ -0,0 +1,21 @@
package org.thoughtcrime.securesms.mediasend;
import android.view.animation.Animation;
/**
* Basic implementation of {@link android.view.animation.Animation.AnimationListener} with empty
* implementation so you don't have to override every method.
*/
public class SimpleAnimationListener implements Animation.AnimationListener {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
}
}

View File

@ -387,7 +387,7 @@ public class AttachmentManager {
.ifNecessary()
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
.onAllGranted(() -> selectMediaType(activity, "image/*", new String[] {"image/*", "video/*"}, requestCode))
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.getIntent(activity, recipient, body, transport), requestCode))
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.buildGalleryIntent(activity, recipient, body, transport), requestCode))
.execute();
}