From 0a8bbf14a633471a07adb73fd7a1675c5469e60d Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 13 Mar 2019 16:05:25 -0700 Subject: [PATCH] Merge camera into send flow. --- AndroidManifest.xml | 5 - res/anim/slide_from_left.xml | 2 +- res/anim/slide_from_right.xml | 2 +- res/anim/slide_to_left.xml | 2 +- res/anim/slide_to_right.xml | 2 +- res/layout/camera_controls_landscape.xml | 4 +- res/layout/camera_fragment.xml | 4 +- res/values/strings.xml | 2 +- .../securesms/camera/Camera1Fragment.java | 13 +- .../securesms/camera/CameraActivity.java | 166 ------------------ .../conversation/ConversationActivity.java | 43 ++--- .../mediasend/MediaPickerFolderFragment.java | 2 +- .../mediasend/MediaPickerItemFragment.java | 2 +- .../mediasend/MediaSendActivity.java | 154 ++++++++++++++-- .../mediasend/MediaSendFragment.java | 4 +- .../mediasend/MediaSendViewModel.java | 83 ++++++++- .../mediasend/SimpleAnimationListener.java | 21 +++ .../securesms/mms/AttachmentManager.java | 2 +- 18 files changed, 267 insertions(+), 246 deletions(-) delete mode 100644 src/org/thoughtcrime/securesms/camera/CameraActivity.java create mode 100644 src/org/thoughtcrime/securesms/mediasend/SimpleAnimationListener.java 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(); }