diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 8ee904d263..7e5a07bf27 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -439,11 +439,6 @@
android:exported="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
-
-
diff --git a/res/anim/slide_from_left.xml b/res/anim/slide_from_left.xml
index 8ad83d09ed..7e00a0272f 100644
--- a/res/anim/slide_from_left.xml
+++ b/res/anim/slide_from_left.xml
@@ -3,7 +3,7 @@
\ No newline at end of file
diff --git a/res/anim/slide_from_right.xml b/res/anim/slide_from_right.xml
index 7dbec61f34..feeaaf7513 100644
--- a/res/anim/slide_from_right.xml
+++ b/res/anim/slide_from_right.xml
@@ -3,7 +3,7 @@
\ No newline at end of file
diff --git a/res/anim/slide_to_left.xml b/res/anim/slide_to_left.xml
index 698746867a..8fd13ee473 100644
--- a/res/anim/slide_to_left.xml
+++ b/res/anim/slide_to_left.xml
@@ -3,7 +3,7 @@
\ No newline at end of file
diff --git a/res/anim/slide_to_right.xml b/res/anim/slide_to_right.xml
index c655fcd12c..0a33f3dd3f 100644
--- a/res/anim/slide_to_right.xml
+++ b/res/anim/slide_to_right.xml
@@ -3,7 +3,7 @@
\ No newline at end of file
diff --git a/res/layout/camera_controls_landscape.xml b/res/layout/camera_controls_landscape.xml
index f0cfd14654..d25e4ed499 100644
--- a/res/layout/camera_controls_landscape.xml
+++ b/res/layout/camera_controls_landscape.xml
@@ -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"
diff --git a/res/layout/camera_fragment.xml b/res/layout/camera_fragment.xml
index 807b425e4c..3ce5bbe7d0 100644
--- a/res/layout/camera_fragment.xml
+++ b/res/layout/camera_fragment.xml
@@ -1,10 +1,10 @@
+ android:layout_height="match_parent"
+ android:background="@color/core_black">
Incoming call
- Camera unavailable.
Failed to save image.
@@ -467,6 +466,7 @@
Add a caption...
An item was removed because it exceeded the size limit
+ Camera unavailable.
All media
diff --git a/src/org/thoughtcrime/securesms/camera/Camera1Fragment.java b/src/org/thoughtcrime/securesms/camera/Camera1Fragment.java
index 0a0c7dfde6..c7fdce0e78 100644
--- a/src/org/thoughtcrime/securesms/camera/Camera1Fragment.java
+++ b/src/org/thoughtcrime/securesms/camera/Camera1Fragment.java
@@ -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();
}
diff --git a/src/org/thoughtcrime/securesms/camera/CameraActivity.java b/src/org/thoughtcrime/securesms/camera/CameraActivity.java
deleted file mode 100644
index a002482618..0000000000
--- a/src/org/thoughtcrime/securesms/camera/CameraActivity.java
+++ /dev/null
@@ -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 result = new SettableFuture<>();
- GlideApp.with(this).load(new DecryptableStreamUriLoader.DecryptableUri(captureUri)).into(new GlideDrawableListeningTarget(snapshot, result));
- result.addListener(new AssertedSuccessListener() {
- @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 message, @NonNull Optional 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) { }
-}
diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
index 7edbca258a..8a91052937 100644
--- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
+++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
@@ -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 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())
diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java b/src/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java
index 08c47a7399..12e84eae0c 100644
--- a/src/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java
+++ b/src/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java
@@ -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
diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java b/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java
index 5927e50a48..0c40542a17 100644
--- a/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java
+++ b/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java
@@ -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
diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java b/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java
index 115aabbc23..507dcda862 100644
--- a/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java
+++ b/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java
@@ -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,
- @NonNull Recipient recipient,
- @NonNull String body,
- @NonNull TransportOption transport)
+ public static Intent buildEditorIntent(@NonNull Context context,
+ @NonNull List 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 = getIntent().getParcelableArrayListExtra(KEY_MEDIA);
+ List 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, @NonNull String message, @NonNull TransportOption transport) {
+ viewModel.onSendClicked();
+
ArrayList 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);
+ }
+
+ }
}
diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java b/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java
index dc81b83c1b..6b5f24e536 100644
--- a/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java
+++ b/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java
@@ -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)) {
diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java b/src/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java
index 9212e1cdb6..b5c2417147 100644
--- a/src/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java
+++ b/src/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java
@@ -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> selectedMedia;
private final MutableLiveData> bucketMedia;
@@ -39,8 +44,11 @@ class MediaSendViewModel extends ViewModel {
private MediaConstraints mediaConstraints;
private CharSequence body;
private CountButtonState.Visibility countButtonVisibility;
+ private boolean sentMedia;
+ private Optional 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 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 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 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 create(@NonNull Class modelClass) {
- return modelClass.cast(new MediaSendViewModel(repository));
+ return modelClass.cast(new MediaSendViewModel(application, repository));
}
}
}
diff --git a/src/org/thoughtcrime/securesms/mediasend/SimpleAnimationListener.java b/src/org/thoughtcrime/securesms/mediasend/SimpleAnimationListener.java
new file mode 100644
index 0000000000..d16be50e9e
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/mediasend/SimpleAnimationListener.java
@@ -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) {
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java
index b1308a8974..a24810f157 100644
--- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java
+++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java
@@ -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();
}