diff --git a/res/drawable-v21/mediarail_button_background.xml b/res/drawable-v21/mediarail_button_background.xml
index 035512fbff..e5c03c2a80 100644
--- a/res/drawable-v21/mediarail_button_background.xml
+++ b/res/drawable-v21/mediarail_button_background.xml
@@ -13,7 +13,7 @@
-
-
+
diff --git a/res/drawable/mediarail_button_background.xml b/res/drawable/mediarail_button_background.xml
index 09e616ea41..57a2ce9857 100644
--- a/res/drawable/mediarail_button_background.xml
+++ b/res/drawable/mediarail_button_background.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/res/layout/camera_controls_landscape.xml b/res/layout/camera_controls_landscape.xml
index de36aeab68..71fb9022d2 100644
--- a/res/layout/camera_controls_landscape.xml
+++ b/res/layout/camera_controls_landscape.xml
@@ -1,32 +1,60 @@
-
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ android:layout_marginEnd="24dp"
+ android:background="@drawable/ic_camera_shutter"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
-
+
+
+
+
+
diff --git a/res/layout/camera_controls_portrait.xml b/res/layout/camera_controls_portrait.xml
index 8e46b2ad4a..5b43bfae47 100644
--- a/res/layout/camera_controls_portrait.xml
+++ b/res/layout/camera_controls_portrait.xml
@@ -1,32 +1,61 @@
-
+ android:layout_height="match_parent">
+ android:layout_marginBottom="24dp"
+ android:background="@drawable/ic_camera_shutter"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+
+
+
-
+
diff --git a/res/layout/mediapicker_folder_fragment.xml b/res/layout/mediapicker_folder_fragment.xml
index 36701cded8..b123509268 100644
--- a/res/layout/mediapicker_folder_fragment.xml
+++ b/res/layout/mediapicker_folder_fragment.xml
@@ -19,6 +19,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="2dp"
- android:layout_marginTop="2dp" />
+ android:layout_marginTop="2dp"
+ android:paddingBottom="@dimen/media_picker_rail_padding_affordance"
+ android:clipToPadding="false" />
\ No newline at end of file
diff --git a/res/layout/mediapicker_item_fragment.xml b/res/layout/mediapicker_item_fragment.xml
index 79283952f9..37f4591e66 100644
--- a/res/layout/mediapicker_item_fragment.xml
+++ b/res/layout/mediapicker_item_fragment.xml
@@ -19,6 +19,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="2dp"
- android:layout_marginTop="2dp" />
+ android:layout_marginTop="2dp"
+ android:paddingBottom="@dimen/media_picker_rail_padding_affordance"
+ android:clipToPadding="false"/>
diff --git a/res/layout/mediasend_activity.xml b/res/layout/mediasend_activity.xml
index 8285e94b4e..6caab7a11d 100644
--- a/res/layout/mediasend_activity.xml
+++ b/res/layout/mediasend_activity.xml
@@ -2,6 +2,7 @@
@@ -11,59 +12,152 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
-
+
-
+ android:layout_gravity="bottom"
+ android:orientation="vertical"
+ android:background="@color/transparent_black_70">
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/mediasend_count_button.xml b/res/layout/mediasend_count_button.xml
new file mode 100644
index 0000000000..388e3fce79
--- /dev/null
+++ b/res/layout/mediasend_count_button.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
diff --git a/res/layout/mediasend_fragment.xml b/res/layout/mediasend_fragment.xml
index 2aa87fcc54..e17b80aea8 100644
--- a/res/layout/mediasend_fragment.xml
+++ b/res/layout/mediasend_fragment.xml
@@ -1,8 +1,6 @@
@@ -19,138 +17,4 @@
android:animateLayoutChanges="true"
android:layout_gravity="top"/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/res/menu/mediapicker_default.xml b/res/menu/mediapicker_default.xml
index f72af9824c..7099f8cec4 100644
--- a/res/menu/mediapicker_default.xml
+++ b/res/menu/mediapicker_default.xml
@@ -5,9 +5,9 @@
\ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 3d17db691c..50d8486a32 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -46,6 +46,7 @@
175dp
85dp
+ 130dp
5dp
4dp
diff --git a/src/org/thoughtcrime/securesms/mediapreview/MediaRailAdapter.java b/src/org/thoughtcrime/securesms/mediapreview/MediaRailAdapter.java
index 9392227db7..22760834f7 100644
--- a/src/org/thoughtcrime/securesms/mediapreview/MediaRailAdapter.java
+++ b/src/org/thoughtcrime/securesms/mediapreview/MediaRailAdapter.java
@@ -24,11 +24,12 @@ public class MediaRailAdapter extends RecyclerView.Adapter media;
private final RailItemListener listener;
- private final boolean editable;
private final StableIdGenerator stableIdGenerator;
private RailItemAddListener addListener;
- private int activePosition;
+ private int activePosition;
+ private boolean editable;
+ private boolean interactive;
public MediaRailAdapter(@NonNull GlideRequests glideRequests, @NonNull RailItemListener listener, boolean editable) {
this.glideRequests = glideRequests;
@@ -36,6 +37,7 @@ public class MediaRailAdapter extends RecyclerView.Adapter();
+ this.interactive = true;
setHasStableIds(true);
}
@@ -57,7 +59,7 @@ public class MediaRailAdapter extends RecyclerView.Adapter railItemListener.onRailItemClicked(distanceFromActive));
- outline.setVisibility(isActive ? View.VISIBLE : View.GONE);
+ outline.setVisibility(isActive && interactive ? View.VISIBLE : View.GONE);
captionIndicator.setVisibility(media.getCaption().isPresent() ? View.VISIBLE : View.GONE);
- if (editable && isActive) {
+ if (editable && isActive && interactive) {
deleteButton.setVisibility(View.VISIBLE);
deleteButton.setOnClickListener(v -> railItemListener.onRailItemDeleteClicked(distanceFromActive));
} else {
diff --git a/src/org/thoughtcrime/securesms/mediasend/Camera1Fragment.java b/src/org/thoughtcrime/securesms/mediasend/Camera1Fragment.java
index b6bd93d17e..5201c8b783 100644
--- a/src/org/thoughtcrime/securesms/mediasend/Camera1Fragment.java
+++ b/src/org/thoughtcrime/securesms/mediasend/Camera1Fragment.java
@@ -27,7 +27,10 @@ import android.view.animation.DecelerateInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.Button;
import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.bumptech.glide.Glide;
import com.bumptech.glide.load.MultiTransformation;
import com.bumptech.glide.load.Transformation;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
@@ -36,10 +39,13 @@ import com.bumptech.glide.request.transition.Transition;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
+import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
+import org.whispersystems.libsignal.util.guava.Optional;
import java.io.ByteArrayOutputStream;
@@ -106,6 +112,9 @@ public class Camera1Fragment extends Fragment implements CameraFragment,
GestureDetector gestureDetector = new GestureDetector(flipGestureListener);
cameraPreview.setOnTouchListener((v, event) -> gestureDetector.onTouchEvent(event));
+
+ viewModel.getMostRecentMediaItem(requireContext()).observe(this, this::presentRecentItemThumbnail);
+ viewModel.getHudState().observe(this, this::presentHud);
}
@Override
@@ -128,9 +137,6 @@ public class Camera1Fragment extends Fragment implements CameraFragment,
});
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
@@ -180,10 +186,46 @@ public class Camera1Fragment extends Fragment implements CameraFragment,
controller.onCameraError();
}
+ private void presentRecentItemThumbnail(Optional media) {
+ if (media == null) {
+ return;
+ }
+
+ ImageView thumbnail = controlsContainer.findViewById(R.id.camera_gallery_button);
+
+ if (media.isPresent()) {
+ thumbnail.setVisibility(View.VISIBLE);
+ Glide.with(this)
+ .load(new DecryptableUri(media.get().getUri()))
+ .centerCrop()
+ .into(thumbnail);
+ } else {
+ thumbnail.setVisibility(View.GONE);
+ thumbnail.setImageResource(0);
+ }
+ }
+
+ private void presentHud(@Nullable MediaSendViewModel.HudState state) {
+ if (state == null) return;
+
+ View countButton = controlsContainer.findViewById(R.id.camera_count_button);
+ TextView countButtonText = controlsContainer.findViewById(R.id.mediasend_count_button_text);
+
+ if (state.getButtonState() == MediaSendViewModel.ButtonState.COUNT) {
+ countButton.setVisibility(View.VISIBLE);
+ countButtonText.setText(String.valueOf(state.getSelectionCount()));
+ } else {
+ countButton.setVisibility(View.GONE);
+ }
+ }
+
@SuppressLint("ClickableViewAccessibility")
private void initControls() {
- flipButton = getView().findViewById(R.id.camera_flip_button);
- captureButton = getView().findViewById(R.id.camera_capture_button);
+ flipButton = requireView().findViewById(R.id.camera_flip_button);
+ captureButton = requireView().findViewById(R.id.camera_capture_button);
+
+ View galleryButton = requireView().findViewById(R.id.camera_gallery_button);
+ View countButton = requireView().findViewById(R.id.camera_count_button);
captureButton.setOnTouchListener((v, event) -> {
switch (event.getAction()) {
@@ -223,6 +265,11 @@ public class Camera1Fragment extends Fragment implements CameraFragment,
flipButton.setVisibility(View.GONE);
}
});
+
+ galleryButton.setOnClickListener(v -> controller.onGalleryClicked());
+ countButton.setOnClickListener(v -> controller.onContinueClicked());
+
+ viewModel.onCameraControlsInitialized();
}
private void onCaptureClicked() {
diff --git a/src/org/thoughtcrime/securesms/mediasend/CameraFragment.java b/src/org/thoughtcrime/securesms/mediasend/CameraFragment.java
index 198d5c192d..e7c2d9240b 100644
--- a/src/org/thoughtcrime/securesms/mediasend/CameraFragment.java
+++ b/src/org/thoughtcrime/securesms/mediasend/CameraFragment.java
@@ -21,6 +21,8 @@ public interface CameraFragment {
interface Controller {
void onCameraError();
void onImageCaptured(@NonNull byte[] data, int width, int height);
+ void onGalleryClicked();
int getDisplayRotation();
+ void onContinueClicked();
}
}
diff --git a/src/org/thoughtcrime/securesms/mediasend/CameraXFragment.java b/src/org/thoughtcrime/securesms/mediasend/CameraXFragment.java
index f2e91b906a..86e0c4eb7b 100644
--- a/src/org/thoughtcrime/securesms/mediasend/CameraXFragment.java
+++ b/src/org/thoughtcrime/securesms/mediasend/CameraXFragment.java
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.os.Bundle;
+import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
@@ -13,6 +14,8 @@ import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.RotateAnimation;
+import android.widget.ImageView;
+import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -23,13 +26,17 @@ import androidx.camera.core.ImageProxy;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
+import com.bumptech.glide.Glide;
+
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXView;
+import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
+import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
@@ -84,6 +91,9 @@ public class CameraXFragment extends Fragment implements CameraFragment {
camera.setCameraLensFacing(CameraXUtil.toLensFacing(TextSecurePreferences.getDirectCaptureCameraId(requireContext())));
onOrientationChanged(getResources().getConfiguration().orientation);
+
+ viewModel.getMostRecentMediaItem(requireContext()).observe(this, this::presentRecentItemThumbnail);
+ viewModel.getHudState().observe(this, this::presentHud);
}
@Override
@@ -116,10 +126,45 @@ public class CameraXFragment extends Fragment implements CameraFragment {
initControls();
}
+ private void presentRecentItemThumbnail(Optional media) {
+ if (media == null) {
+ return;
+ }
+
+ ImageView thumbnail = controlsContainer.findViewById(R.id.camera_gallery_button);
+
+ if (media.isPresent()) {
+ thumbnail.setVisibility(View.VISIBLE);
+ Glide.with(this)
+ .load(new DecryptableUri(media.get().getUri()))
+ .centerCrop()
+ .into(thumbnail);
+ } else {
+ thumbnail.setVisibility(View.GONE);
+ thumbnail.setImageResource(0);
+ }
+ }
+
+ private void presentHud(@Nullable MediaSendViewModel.HudState state) {
+ if (state == null) return;
+
+ View countButton = controlsContainer.findViewById(R.id.camera_count_button);
+ TextView countButtonText = controlsContainer.findViewById(R.id.mediasend_count_button_text);
+
+ if (state.getButtonState() == MediaSendViewModel.ButtonState.COUNT) {
+ countButton.setVisibility(View.VISIBLE);
+ countButtonText.setText(String.valueOf(state.getSelectionCount()));
+ } else {
+ countButton.setVisibility(View.GONE);
+ }
+ }
+
@SuppressLint({"ClickableViewAccessibility", "MissingPermission"})
private void initControls() {
View flipButton = requireView().findViewById(R.id.camera_flip_button);
View captureButton = requireView().findViewById(R.id.camera_capture_button);
+ View galleryButton = requireView().findViewById(R.id.camera_gallery_button);
+ View countButton = requireView().findViewById(R.id.camera_count_button);
captureButton.setOnTouchListener((v, event) -> {
switch (event.getAction()) {
@@ -154,9 +199,26 @@ public class CameraXFragment extends Fragment implements CameraFragment {
animation.setInterpolator(new DecelerateInterpolator());
flipButton.startAnimation(animation);
});
+
+ GestureDetector gestureDetector = new GestureDetector(requireContext(), new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ flipButton.performClick();
+ return true;
+ }
+ });
+
+ camera.setOnTouchListener((v, event) -> gestureDetector.onTouchEvent(event));
+
+
} else {
flipButton.setVisibility(View.GONE);
}
+
+ galleryButton.setOnClickListener(v -> controller.onGalleryClicked());
+ countButton.setOnClickListener(v -> controller.onContinueClicked());
+
+ viewModel.onCameraControlsInitialized();
}
private void onCaptureClicked() {
diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java b/src/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java
index e46711306a..3f448bdf74 100644
--- a/src/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java
+++ b/src/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java
@@ -13,6 +13,8 @@ import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.appcompat.widget.Toolbar;
import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
@@ -51,6 +53,8 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+
recipientName = getArguments().getString(KEY_RECIPIENT_NAME);
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
}
@@ -92,14 +96,26 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
@Override
public void onResume() {
super.onResume();
-
viewModel.onFolderPickerStarted();
- requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
- requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
@Override
- public void onConfigurationChanged(Configuration newConfig) {
+ public void onPrepareOptionsMenu(@NonNull Menu menu) {
+ requireActivity().getMenuInflater().inflate(R.menu.mediapicker_default, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.mediapicker_menu_camera:
+ controller.onCameraSelected();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
onScreenWidthChanged(getScreenWidth());
}
@@ -131,5 +147,6 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
public interface Controller {
void onFolderSelected(@NonNull MediaFolder folder);
+ void onCameraSelected();
}
}
diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java b/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java
index fbdad89a0c..09a026643a 100644
--- a/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java
+++ b/src/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java
@@ -104,41 +104,33 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
}
viewModel.getMediaInBucket(requireContext(), bucketId).observe(this, adapter::setMedia);
-
- initMediaObserver(viewModel);
}
@Override
public void onResume() {
super.onResume();
-
viewModel.onItemPickerStarted();
- requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
- requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ adapter.setForcedMultiSelect(true);
+ viewModel.onMultiSelectStarted();
}
@Override
- public void onPrepareOptionsMenu(Menu menu) {
+ public void onPrepareOptionsMenu(@NonNull Menu menu) {
requireActivity().getMenuInflater().inflate(R.menu.mediapicker_default, menu);
-
- if (viewModel.getCountButtonState().getValue() != null && viewModel.getCountButtonState().getValue().isVisible()) {
- menu.findItem(R.id.mediapicker_menu_add).setVisible(false);
- }
}
@Override
- public boolean onOptionsItemSelected(MenuItem item) {
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
- case R.id.mediapicker_menu_add:
- adapter.setForcedMultiSelect(true);
- viewModel.onMultiSelectStarted();
+ case R.id.mediapicker_menu_camera:
+ controller.onCameraSelected();
return true;
}
return false;
}
@Override
- public void onConfigurationChanged(Configuration newConfig) {
+ public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
onScreenWidthChanged(getScreenWidth());
}
@@ -172,12 +164,6 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed());
}
- private void initMediaObserver(@NonNull MediaSendViewModel viewModel) {
- viewModel.getCountButtonState().observe(this, media -> {
- requireActivity().invalidateOptionsMenu();
- });
- }
-
private void onScreenWidthChanged(int newWidth) {
if (layoutManager != null) {
layoutManager.setSpanCount(newWidth / getResources().getDimensionPixelSize(R.dimen.media_picker_item_width));
@@ -192,5 +178,6 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
public interface Controller {
void onMediaSelected(@NonNull Media media);
+ void onCameraSelected();
}
}
diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaRepository.java b/src/org/thoughtcrime/securesms/mediasend/MediaRepository.java
index ce75dbb6b8..87c75f2ade 100644
--- a/src/org/thoughtcrime/securesms/mediasend/MediaRepository.java
+++ b/src/org/thoughtcrime/securesms/mediasend/MediaRepository.java
@@ -4,7 +4,6 @@ import android.annotation.TargetApi;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
-import android.os.AsyncTask;
import android.os.Environment;
import android.provider.MediaStore.Images;
import android.provider.MediaStore.Video;
@@ -20,6 +19,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
+import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.File;
@@ -40,14 +40,14 @@ class MediaRepository {
* Retrieves a list of folders that contain media.
*/
void getFolders(@NonNull Context context, @NonNull Callback> callback) {
- AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> callback.onComplete(getFolders(context)));
+ SignalExecutors.BOUNDED.execute(() -> callback.onComplete(getFolders(context)));
}
/**
* Retrieves a list of media items (images and videos) that are present int he specified bucket.
*/
void getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Callback> callback) {
- AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> callback.onComplete(getMediaInBucket(context, bucketId)));
+ SignalExecutors.BOUNDED.execute(() -> callback.onComplete(getMediaInBucket(context, bucketId)));
}
/**
@@ -60,7 +60,11 @@ class MediaRepository {
return;
}
- AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> callback.onComplete(getPopulatedMedia(context, media)));
+ SignalExecutors.BOUNDED.execute(() -> callback.onComplete(getPopulatedMedia(context, media)));
+ }
+
+ void getMostRecentItem(@NonNull Context context, @NonNull Callback> callback) {
+ SignalExecutors.BOUNDED.execute(() -> callback.onComplete(getMostRecentItem(context)));
}
@WorkerThread
@@ -158,7 +162,7 @@ class MediaRepository {
}
@WorkerThread
- private @NonNull List getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Uri contentUri, boolean hasOrienation) {
+ private @NonNull List getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Uri contentUri, boolean hasOrientation) {
List media = new LinkedList<>();
String selection = Images.Media.BUCKET_ID + " = ? AND " + Images.Media.DATA + " NOT NULL";
String[] selectionArgs = new String[] { bucketId };
@@ -166,7 +170,7 @@ class MediaRepository {
String[] projection;
- if (hasOrienation) {
+ if (hasOrientation) {
projection = new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.ORIENTATION, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE};
} else {
projection = new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE};
@@ -182,7 +186,7 @@ class MediaRepository {
Uri uri = Uri.withAppendedPath(contentUri, cursor.getString(cursor.getColumnIndexOrThrow(Images.Media._ID)));
String mimetype = cursor.getString(cursor.getColumnIndexOrThrow(Images.Media.MIME_TYPE));
long dateTaken = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.DATE_TAKEN));
- int orientation = hasOrienation ? cursor.getInt(cursor.getColumnIndexOrThrow(Images.Media.ORIENTATION)) : 0;
+ int orientation = hasOrientation ? cursor.getInt(cursor.getColumnIndexOrThrow(Images.Media.ORIENTATION)) : 0;
int width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation)));
int height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation)));
long size = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.SIZE));
@@ -211,6 +215,12 @@ class MediaRepository {
}).toList();
}
+ @WorkerThread
+ private Optional getMostRecentItem(@NonNull Context context) {
+ List media = getMediaInBucket(context, Media.ALL_MEDIA_BUCKET_ID, Images.Media.EXTERNAL_CONTENT_URI, true);
+ return media.size() > 0 ? Optional.of(media.get(0)) : Optional.absent();
+ }
+
@TargetApi(16)
@SuppressWarnings("SuspiciousNameCombination")
private String getWidthColumn(int orientation) {
diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java b/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java
index 5809ccc212..21341be3a9 100644
--- a/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java
+++ b/src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java
@@ -1,45 +1,76 @@
package org.thoughtcrime.securesms.mediasend;
-import android.Manifest;
-
-import androidx.fragment.app.FragmentTransaction;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.view.ContextThemeWrapper;
import androidx.lifecycle.ViewModelProviders;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
import android.net.Uri;
+import android.os.AsyncTask;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.KeyEvent;
import android.view.View;
-import android.view.animation.AccelerateDecelerateInterpolator;
-import android.view.animation.AccelerateInterpolator;
-import android.view.animation.Animation;
-import android.view.animation.DecelerateInterpolator;
-import android.view.animation.OvershootInterpolator;
-import android.view.animation.ScaleAnimation;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.inputmethod.EditorInfo;
+import android.widget.ImageView;
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.components.ComposeText;
+import org.thoughtcrime.securesms.components.InputAwareLayout;
+import org.thoughtcrime.securesms.components.SendButton;
+import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
+import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
+import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
+import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
+import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
import org.thoughtcrime.securesms.database.Address;
+import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
+import org.thoughtcrime.securesms.mediasend.MediaSendViewModel.TimerState;
+import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment;
+import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.MediaUtil;
+import org.thoughtcrime.securesms.util.Stopwatch;
+import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
+import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
+import org.thoughtcrime.securesms.util.views.Stub;
import org.whispersystems.libsignal.util.guava.Optional;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
/**
* Encompasses the entire flow of sending media, starting from the selection process to the actual
@@ -50,15 +81,19 @@ import java.util.Locale;
*/
public class MediaSendActivity extends PassphraseRequiredActionBarActivity implements MediaPickerFolderFragment.Controller,
MediaPickerItemFragment.Controller,
- MediaSendFragment.Controller,
ImageEditorFragment.Controller,
- CameraFragment.Controller
+ CameraFragment.Controller,
+ ViewTreeObserver.OnGlobalLayoutListener,
+ MediaRailAdapter.RailItemListener,
+ InputAwareLayout.OnKeyboardShownListener,
+ InputAwareLayout.OnKeyboardHiddenListener
{
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";
+ public static final String EXTRA_MEDIA = "media";
+ public static final String EXTRA_MESSAGE = "message";
+ public static final String EXTRA_TRANSPORT = "transport";
+ public static final String EXTRA_REVEAL_DURATION = "reveal_duration";
private static final String KEY_ADDRESS = "address";
@@ -78,9 +113,25 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
private TransportOption transport;
private MediaSendViewModel viewModel;
- private View countButton;
- private TextView countButtonText;
- private View cameraButton;
+ private InputAwareLayout hud;
+ private View captionAndRail;
+ private SendButton sendButton;
+ private ViewGroup sendButtonContainer;
+ private ComposeText composeText;
+ private ViewGroup composeRow;
+ private ViewGroup composeContainer;
+ private ViewGroup countButton;
+ private TextView countButtonText;
+ private EmojiEditText captionText;
+ private EmojiToggle emojiToggle;
+ private Stub emojiDrawer;
+ private TextView charactersLeft;
+ private RecyclerView mediaRail;
+ private MediaRailAdapter mediaRailAdapter;
+
+ private int visibleHeight;
+
+ private final Rect visibleBounds = new Rect();
/**
* Get an intent to launch the media send flow starting with the picker.
@@ -131,15 +182,27 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
return;
}
- countButton = findViewById(R.id.mediasend_count_button);
- countButtonText = findViewById(R.id.mediasend_count_button_text);
- cameraButton = findViewById(R.id.mediasend_camera_button);
+ hud = findViewById(R.id.mediasend_hud);
+ captionAndRail = findViewById(R.id.mediasend_caption_and_rail);
+ sendButton = findViewById(R.id.mediasend_send_button);
+ sendButtonContainer = findViewById(R.id.mediasend_send_button_bkg);
+ composeText = findViewById(R.id.mediasend_compose_text);
+ composeRow = findViewById(R.id.mediasend_compose_row);
+ composeContainer = findViewById(R.id.mediasend_compose_container);
+ countButton = findViewById(R.id.mediasend_count_button);
+ countButtonText = findViewById(R.id.mediasend_count_button_text);
+ captionText = findViewById(R.id.mediasend_caption);
+ emojiToggle = findViewById(R.id.mediasend_emoji_toggle);
+ charactersLeft = findViewById(R.id.mediasend_characters_left);
+ mediaRail = findViewById(R.id.mediasend_media_rail);
+ emojiDrawer = new Stub<>(findViewById(R.id.mediasend_emoji_drawer_stub));
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);
viewModel.setTransport(transport);
+ viewModel.setRecipient(recipient);
viewModel.onBodyChanged(getIntent().getStringExtra(KEY_BODY));
List media = getIntent().getParcelableArrayListExtra(KEY_MEDIA);
@@ -165,19 +228,80 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
.commit();
}
- initializeCountButtonObserver(transport, dynamicLanguage.getCurrentLocale());
- initializeCameraButtonObserver();
- initializeErrorObserver();
+ sendButton.setOnClickListener(v -> {
+ if (hud.isKeyboardOpen()) {
+ hud.hideSoftkey(composeText, null);
+ }
- cameraButton.setOnClickListener(v -> {
- int maxSelection = viewModel.getMaxSelection();
+ MediaSendFragment fragment = getMediaSendFragment();
- if (viewModel.getSelectedMedia().getValue() != null && viewModel.getSelectedMedia().getValue().size() >= maxSelection) {
- Toast.makeText(this, getResources().getQuantityString(R.plurals.MediaSendActivity_cant_share_more_than_n_items, maxSelection, maxSelection), Toast.LENGTH_SHORT).show();
+ if (fragment != null) {
+ processMedia(fragment.getAllMedia(), fragment.getSavedState());
} else {
- navigateToCamera();
+ throw new AssertionError("No send fragment available!");
}
});
+
+ sendButton.addOnTransportChangedListener((newTransport, manuallySelected) -> {
+ presentCharactersRemaining();
+ composeText.setTransport(newTransport);
+ sendButtonContainer.getBackground().setColorFilter(newTransport.getBackgroundColor(), PorterDuff.Mode.MULTIPLY);
+ sendButtonContainer.getBackground().invalidateSelf();
+ });
+
+ ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener();
+
+ composeText.setOnKeyListener(composeKeyPressedListener);
+ composeText.addTextChangedListener(composeKeyPressedListener);
+ composeText.setOnClickListener(composeKeyPressedListener);
+ composeText.setOnFocusChangeListener(composeKeyPressedListener);
+
+ captionText.clearFocus();
+ composeText.requestFocus();
+
+ mediaRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, true);
+ mediaRail.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
+ mediaRail.setAdapter(mediaRailAdapter);
+
+ hud.getRootView().getViewTreeObserver().addOnGlobalLayoutListener(this);
+ hud.addOnKeyboardShownListener(this);
+ hud.addOnKeyboardHiddenListener(this);
+
+ captionText.addTextChangedListener(new SimpleTextWatcher() {
+ @Override
+ public void onTextChanged(String text) {
+ viewModel.onCaptionChanged(text);
+ }
+ });
+
+ sendButton.setTransport(transport);
+ sendButton.disableTransport(transport.getType() == TransportOption.Type.SMS ? TransportOption.Type.TEXTSECURE : TransportOption.Type.SMS);
+
+ countButton.setOnClickListener(v -> navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale()));
+
+ composeText.append(viewModel.getBody());
+
+ if (recipient.isLocalNumber()) {
+ composeText.setHint(getString(R.string.note_to_self), null);
+ } else {
+ String displayName = Optional.fromNullable(recipient.getName())
+ .or(Optional.fromNullable(recipient.getProfileName())
+ .or(recipient.getAddress().serialize()));
+ composeText.setHint(getString(R.string.MediaSendActivity_message_to_s, displayName), null);
+ }
+ composeText.setOnEditorActionListener((v, actionId, event) -> {
+ boolean isSend = actionId == EditorInfo.IME_ACTION_SEND;
+ if (isSend) sendButton.performClick();
+ return isSend;
+ });
+
+ if (TextSecurePreferences.isSystemEmojiPreferred(this)) {
+ emojiToggle.setVisibility(View.GONE);
+ } else {
+ emojiToggle.setOnClickListener(this::onEmojiToggleClicked);
+ }
+
+ initViewModel();
}
@Override
@@ -188,13 +312,12 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
@Override
public void onBackPressed() {
- MediaSendFragment sendFragment = (MediaSendFragment) getSupportFragmentManager().findFragmentByTag(TAG_SEND);
- if (sendFragment == null || !sendFragment.isVisible() || !sendFragment.handleBackPress()) {
- super.onBackPressed();
+ MediaSendFragment sendFragment = (MediaSendFragment) getSupportFragmentManager().findFragmentByTag(TAG_SEND);
- if (getIntent().getBooleanExtra(KEY_IS_CAMERA, false) && getSupportFragmentManager().getBackStackEntryCount() == 0) {
- viewModel.onImageCaptureUndo(this);
- }
+ if (sendFragment == null || !sendFragment.isVisible() || !hud.isInputOpen()) {
+ super.onBackPressed();
+ } else {
+ hud.hideCurrentInput(composeText);
}
}
@@ -209,7 +332,6 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
MediaPickerItemFragment fragment = MediaPickerItemFragment.newInstance(folder.getBucketId(), folder.getTitle(), viewModel.getMaxSelection());
getSupportFragmentManager().beginTransaction()
- .setCustomAnimations(R.anim.slide_from_right, R.anim.slide_to_left, R.anim.slide_from_left, R.anim.slide_to_right)
.replace(R.id.mediasend_fragment_container, fragment, TAG_ITEM_PICKER)
.addToBackStack(null)
.commit();
@@ -218,48 +340,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
@Override
public void onMediaSelected(@NonNull Media media) {
viewModel.onSingleMediaSelected(this, media);
- navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale(), false);
- }
-
- @Override
- public void onAddMediaClicked(@NonNull String bucketId) {
- // TODO: Get actual folder title somehow
- MediaPickerFolderFragment folderFragment = MediaPickerFolderFragment.newInstance(recipient);
- MediaPickerItemFragment itemFragment = MediaPickerItemFragment.newInstance(bucketId, "", viewModel.getMaxSelection());
-
- getSupportFragmentManager().beginTransaction()
- .setCustomAnimations(R.anim.stationary, R.anim.slide_to_left, R.anim.slide_from_left, R.anim.slide_to_right)
- .replace(R.id.mediasend_fragment_container, folderFragment, TAG_FOLDER_PICKER)
- .addToBackStack(null)
- .commit();
-
- getSupportFragmentManager().beginTransaction()
- .setCustomAnimations(R.anim.slide_from_right, R.anim.stationary, R.anim.slide_from_left, R.anim.slide_to_right)
- .replace(R.id.mediasend_fragment_container, itemFragment, TAG_ITEM_PICKER)
- .addToBackStack(null)
- .commit();
- }
-
- @Override
- public void onSendClicked(@NonNull List media, @NonNull String message, @NonNull TransportOption transport) {
- viewModel.onSendClicked();
-
- ArrayList mediaList = new ArrayList<>(media);
- Intent intent = new Intent();
-
- intent.putParcelableArrayListExtra(EXTRA_MEDIA, mediaList);
- intent.putExtra(EXTRA_MESSAGE, message);
- intent.putExtra(EXTRA_TRANSPORT, transport);
- setResult(RESULT_OK, intent);
- finish();
-
- overridePendingTransition(R.anim.stationary, R.anim.camera_slide_to_bottom);
- }
-
- @Override
- public void onNoMediaAvailable() {
- setResult(RESULT_CANCELED);
- finish();
+ navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale());
}
@Override
@@ -307,7 +388,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
Log.i(TAG, "Camera capture stored: " + media.getUri().toString());
viewModel.onImageCaptured(media);
- navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale(), true);
+ navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale());
});
}
@@ -316,37 +397,203 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
return getWindowManager().getDefaultDisplay().getRotation();
}
- private void initializeCountButtonObserver(@NonNull TransportOption transport, @NonNull Locale locale) {
- viewModel.getCountButtonState().observe(this, buttonState -> {
- if (buttonState == null) return;
+ @Override
+ public void onContinueClicked() {
+ navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale());
+ }
- countButtonText.setText(String.valueOf(buttonState.getCount()));
- countButton.setEnabled(buttonState.isVisible());
- animateButtonVisibility(countButton, countButton.getVisibility(), buttonState.isVisible() ? View.VISIBLE : View.GONE);
+ @Override
+ public void onGalleryClicked() {
+ MediaPickerFolderFragment folderFragment = MediaPickerFolderFragment.newInstance(recipient);
- if (buttonState.getCount() > 0) {
- countButton.setOnClickListener(v -> navigateToMediaSend(recipient, transport, locale, false));
- if (buttonState.isVisible()) {
- animateButtonTextChange(countButton);
- }
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.mediasend_fragment_container, folderFragment, TAG_FOLDER_PICKER)
+ .setCustomAnimations(R.anim.slide_from_bottom, R.anim.stationary, R.anim.slide_to_bottom, R.anim.stationary)
+ .addToBackStack(null)
+ .commit();
+ }
+
+ @Override
+ public void onRequestFullScreen(boolean fullScreen) {
+ captionAndRail.setVisibility(fullScreen ? View.GONE : View.VISIBLE);
+ }
+
+ @Override
+ public void onGlobalLayout() {
+ hud.getRootView().getWindowVisibleDisplayFrame(visibleBounds);
+
+ int currentVisibleHeight = visibleBounds.height();
+
+ if (currentVisibleHeight != visibleHeight) {
+ hud.getLayoutParams().height = currentVisibleHeight;
+ hud.layout(visibleBounds.left, visibleBounds.top, visibleBounds.right, visibleBounds.bottom);
+ hud.requestLayout();
+
+ visibleHeight = currentVisibleHeight;
+ }
+ }
+
+ @Override
+ public void onKeyboardHidden() {
+ viewModel.onKeyboardHidden(sendButton.getSelectedTransport().isSms());
+ }
+
+ @Override
+ public void onKeyboardShown() {
+ viewModel.onKeyboardShown(composeText.hasFocus(), captionText.hasFocus(), sendButton.getSelectedTransport().isSms());
+ }
+
+ @Override
+ public void onRailItemClicked(int distanceFromActive) {
+ if (getMediaSendFragment() != null) {
+ viewModel.onPageChanged(getMediaSendFragment().getCurrentImagePosition() + distanceFromActive);
+ }
+ }
+
+ @Override
+ public void onRailItemDeleteClicked(int distanceFromActive) {
+ if (getMediaSendFragment() != null) {
+ viewModel.onMediaItemRemoved(this, getMediaSendFragment().getCurrentImagePosition() + distanceFromActive);
+ }
+ }
+
+ @Override
+ public void onCameraSelected() {
+ navigateToCamera();
+ }
+
+ public void onAddMediaClicked(@NonNull String bucketId) {
+ // TODO: Get actual folder title somehow
+ MediaPickerFolderFragment folderFragment = MediaPickerFolderFragment.newInstance(recipient);
+ MediaPickerItemFragment itemFragment = MediaPickerItemFragment.newInstance(bucketId, "", viewModel.getMaxSelection());
+
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.mediasend_fragment_container, folderFragment, TAG_FOLDER_PICKER)
+ .addToBackStack(null)
+ .commit();
+
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.mediasend_fragment_container, itemFragment, TAG_ITEM_PICKER)
+ .addToBackStack(null)
+ .commit();
+ }
+
+ public void onSendClicked(@NonNull List media, @NonNull String message, @NonNull TransportOption transport) {
+ viewModel.onSendClicked();
+
+ ArrayList mediaList = new ArrayList<>(media);
+
+ if (mediaList.size() > 0) {
+ Intent intent = new Intent();
+
+ intent.putParcelableArrayListExtra(EXTRA_MEDIA, mediaList);
+ intent.putExtra(EXTRA_MESSAGE, viewModel.getRevealDuration() == 0 ? message : "");
+ intent.putExtra(EXTRA_TRANSPORT, transport);
+ intent.putExtra(EXTRA_REVEAL_DURATION, viewModel.getRevealDuration());
+
+ setResult(RESULT_OK, intent);
+ } else {
+ setResult(RESULT_CANCELED);
+ }
+
+ finish();
+
+ overridePendingTransition(R.anim.stationary, R.anim.camera_slide_to_bottom);
+ }
+
+ public void onNoMediaAvailable() {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+
+
+ private void initViewModel() {
+ viewModel.getHudState().observe(this, state -> {
+ if (state == null) return;
+
+ hud.setVisibility(state.isHudVisible() ? View.VISIBLE : View.GONE);
+ composeContainer.setVisibility(state.isComposeVisible() ? View.VISIBLE : (state.getTimerState() == TimerState.GONE ? View.GONE : View.INVISIBLE));
+ captionText.setVisibility(state.isCaptionVisible() ? View.VISIBLE : View.GONE);
+
+ int captionBackground;
+
+ if (state.getRailState() == MediaSendViewModel.RailState.VIEWABLE) {
+ captionBackground = R.color.core_grey_90;
+ } else if (state.getTimerState() == TimerState.ENABLED) {
+ captionBackground = 0;
} else {
- countButton.setOnClickListener(null);
+ captionBackground = R.color.transparent_black_70;
+ }
+
+ captionAndRail.setBackgroundResource(captionBackground);
+
+ switch (state.getButtonState()) {
+ case SEND:
+ sendButtonContainer.setVisibility(View.VISIBLE);
+ countButton.setVisibility(View.GONE);
+ break;
+ case COUNT:
+ sendButtonContainer.setVisibility(View.GONE);
+ countButton.setVisibility(View.VISIBLE);
+ countButtonText.setText(String.valueOf(state.getSelectionCount()));
+ break;
+ case GONE:
+ sendButtonContainer.setVisibility(View.GONE);
+ countButton.setVisibility(View.GONE);
+ break;
+ }
+
+ switch (state.getRailState()) {
+ case INTERACTIVE:
+ mediaRail.setVisibility(View.VISIBLE);
+ mediaRailAdapter.setEditable(true);
+ mediaRailAdapter.setInteractive(true);
+ break;
+ case VIEWABLE:
+ mediaRail.setVisibility(View.VISIBLE);
+ mediaRailAdapter.setEditable(false);
+ mediaRailAdapter.setInteractive(false);
+ break;
+ case GONE:
+ mediaRail.setVisibility(View.GONE);
+ break;
+ }
+
+ if (composeContainer.getVisibility() == View.GONE && sendButtonContainer.getVisibility() == View.GONE) {
+ composeRow.setVisibility(View.GONE);
+ } else {
+ composeRow.setVisibility(View.VISIBLE);
}
});
- }
- private void initializeCameraButtonObserver() {
- viewModel.getCameraButtonVisibility().observe(this, visible -> {
- if (visible == null) return;
- animateButtonVisibility(cameraButton, cameraButton.getVisibility(), visible ? View.VISIBLE : View.GONE);
+ viewModel.getSelectedMedia().observe(this, media -> {
+ mediaRailAdapter.setMedia(media);
+ });
+
+ viewModel.getPosition().observe(this, position -> {
+ if (position == null || position < 0) return;
+
+ MediaSendFragment fragment = getMediaSendFragment();
+ if (fragment != null && fragment.getAllMedia().size() > position) {
+ captionText.setText(fragment.getAllMedia().get(position).getCaption().or(""));
+ }
+
+ mediaRailAdapter.setActivePosition(position);
+ mediaRail.smoothScrollToPosition(position);
+ });
+
+ viewModel.getBucketId().observe(this, bucketId -> {
+ if (bucketId == null) return;
+ mediaRailAdapter.setAddButtonListener(() -> onAddMediaClicked(bucketId));
});
- }
- private void initializeErrorObserver() {
viewModel.getError().observe(this, error -> {
if (error == null) return;
switch (error) {
+ case NO_ITEMS:
+ onNoMediaAvailable();
+ break;
case ITEM_TOO_LARGE:
Toast.makeText(this, R.string.MediaSendActivity_an_item_was_removed_because_it_exceeded_the_size_limit, Toast.LENGTH_LONG).show();
break;
@@ -358,7 +605,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
});
}
- private void navigateToMediaSend(@NonNull Recipient recipient, @NonNull TransportOption transport, @NonNull Locale locale, boolean fade) {
+ private void navigateToMediaSend(@NonNull Recipient recipient, @NonNull TransportOption transport, @NonNull Locale locale) {
MediaSendFragment fragment = MediaSendFragment.newInstance(recipient, transport, locale);
String backstackTag = null;
@@ -367,17 +614,11 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
backstackTag = TAG_SEND;
}
- FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
-
- if (fade) {
- transaction.setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_out, R.anim.fade_in);
- } else {
- transaction.setCustomAnimations(R.anim.slide_from_right, R.anim.slide_to_left, R.anim.slide_from_left, R.anim.slide_to_right);
- }
-
- transaction.replace(R.id.mediasend_fragment_container, fragment, TAG_SEND)
- .addToBackStack(backstackTag)
- .commit();
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.mediasend_fragment_container, fragment, TAG_SEND)
+ .setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
+ .addToBackStack(backstackTag)
+ .commit();
}
private void navigateToCamera() {
@@ -389,7 +630,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
.onAllGranted(() -> {
Fragment fragment = getOrCreateCameraFragment();
getSupportFragmentManager().beginTransaction()
- .setCustomAnimations(R.anim.slide_from_right, R.anim.slide_to_left, R.anim.slide_from_left, R.anim.slide_to_right)
+ .setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
.replace(R.id.mediasend_fragment_container, fragment, TAG_CAMERA)
.addToBackStack(null)
.commit();
@@ -400,66 +641,182 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
private Fragment getOrCreateCameraFragment() {
Fragment fragment = getSupportFragmentManager().findFragmentByTag(TAG_CAMERA);
-
- return fragment != null ? fragment
- : CameraFragment.newInstance();
+ return fragment != null ? fragment : CameraFragment.newInstance();
}
- private void animateButtonVisibility(@NonNull View button, int oldVisibility, int newVisibility) {
- if (oldVisibility == newVisibility) return;
+ private EmojiEditText getActiveInputField() {
+ if (captionText.hasFocus()) return captionText;
+ else return composeText;
+ }
- if (button.getAnimation() != null) {
- button.clearAnimation();
- button.setVisibility(newVisibility);
- } else if (newVisibility == View.VISIBLE) {
- button.setVisibility(View.VISIBLE);
- Animation animation = new ScaleAnimation(0, 1, 0, 1, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
- animation.setDuration(250);
- animation.setInterpolator(new OvershootInterpolator());
- button.startAnimation(animation);
+ private void presentCharactersRemaining() {
+ String messageBody = composeText.getTextTrimmed();
+ TransportOption transportOption = sendButton.getSelectedTransport();
+ CharacterState characterState = transportOption.calculateCharacters(messageBody);
+
+ if (characterState.charactersRemaining <= 15 || characterState.messagesSpent > 1) {
+ charactersLeft.setText(String.format(dynamicLanguage.getCurrentLocale(),
+ "%d/%d (%d)",
+ characterState.charactersRemaining,
+ characterState.maxTotalMessageSize,
+ characterState.messagesSpent));
+ charactersLeft.setVisibility(View.VISIBLE);
} 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() {
+ charactersLeft.setVisibility(View.GONE);
+ }
+ }
+
+
+ private void onEmojiToggleClicked(View v) {
+ if (!emojiDrawer.resolved()) {
+ emojiDrawer.get().setProviders(0, new EmojiKeyboardProvider(this, new EmojiKeyboardProvider.EmojiEventListener() {
@Override
- public void onAnimationEnd(Animation animation) {
- button.clearAnimation();
- button.setVisibility(View.GONE);
+ public void onKeyEvent(KeyEvent keyEvent) {
+ getActiveInputField().dispatchKeyEvent(keyEvent);
}
- });
- button.startAnimation(animation);
+ @Override
+ public void onEmojiSelected(String emoji) {
+ getActiveInputField().insertEmoji(emoji);
+ }
+ }));
+ emojiToggle.attach(emojiDrawer.get());
+ }
+
+ if (hud.getCurrentInput() == emojiDrawer.get()) {
+ hud.showSoftkey(composeText);
+ } else {
+ hud.hideSoftkey(composeText, () -> hud.post(() -> hud.show(composeText, emojiDrawer.get())));
}
}
- private void animateButtonTextChange(@NonNull View button) {
- if (button.getAnimation() != null) {
- button.clearAnimation();
- }
+ @SuppressLint("StaticFieldLeak")
+ private void processMedia(@NonNull List mediaList, @NonNull Map savedState) {
+ Map modelsToRender = new HashMap<>();
- Animation grow = new ScaleAnimation(1f, 1.3f, 1f, 1.3f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
- grow.setDuration(125);
- grow.setInterpolator(new AccelerateInterpolator());
- grow.setAnimationListener(new SimpleAnimationListener() {
- @Override
- public void onAnimationEnd(Animation animation) {
- Animation shrink = new ScaleAnimation(1.3f, 1f, 1.3f, 1f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
- shrink.setDuration(125);
- shrink.setInterpolator(new DecelerateInterpolator());
- button.startAnimation(shrink);
+ for (Media media : mediaList) {
+ Object state = savedState.get(media.getUri());
+
+ if (state instanceof ImageEditorFragment.Data) {
+ EditorModel model = ((ImageEditorFragment.Data) state).readModel();
+ if (model != null && model.isChanged()) {
+ modelsToRender.put(media, model);
+ }
}
- });
+ }
- button.startAnimation(grow);
+ new AsyncTask>() {
+
+ private Stopwatch renderTimer;
+ private Runnable progressTimer;
+ private AlertDialog dialog;
+
+ @Override
+ protected void onPreExecute() {
+ renderTimer = new Stopwatch("ProcessMedia");
+ progressTimer = () -> {
+ dialog = new AlertDialog.Builder(new ContextThemeWrapper(MediaSendActivity.this, R.style.TextSecure_MediaSendProgressDialog))
+ .setView(R.layout.progress_dialog)
+ .setCancelable(false)
+ .create();
+ dialog.show();
+ dialog.getWindow().setLayout(getResources().getDimensionPixelSize(R.dimen.mediasend_progress_dialog_size),
+ getResources().getDimensionPixelSize(R.dimen.mediasend_progress_dialog_size));
+ };
+ Util.runOnMainDelayed(progressTimer, 250);
+ }
+
+ @Override
+ protected List doInBackground(Void... voids) {
+ Context context = MediaSendActivity.this;
+ List updatedMedia = new ArrayList<>(mediaList.size());
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+
+ for (Media media : mediaList) {
+ EditorModel modelToRender = modelsToRender.get(media);
+ if (modelToRender != null) {
+ Bitmap bitmap = modelToRender.render(context);
+ try {
+ outputStream.reset();
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
+
+ Uri uri = BlobProvider.getInstance()
+ .forData(outputStream.toByteArray())
+ .withMimeType(MediaUtil.IMAGE_JPEG)
+ .createForSingleSessionOnDisk(context, e -> Log.w(TAG, "Failed to write to disk.", e));
+
+ Media updated = new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), outputStream.size(), media.getBucketId(), media.getCaption());
+
+ updatedMedia.add(updated);
+ renderTimer.split("item");
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to render image. Using base image.");
+ updatedMedia.add(media);
+ } finally {
+ bitmap.recycle();
+ }
+ } else {
+ updatedMedia.add(media);
+ }
+ }
+ return updatedMedia;
+ }
+
+ @Override
+ protected void onPostExecute(List media) {
+ onSendClicked(media, composeText.getTextTrimmed(), sendButton.getSelectedTransport());
+ Util.cancelRunnableOnMain(progressTimer);
+ if (dialog != null) {
+ dialog.dismiss();
+ }
+ renderTimer.stop(TAG);
+ }
+ }.executeOnExecutor(SignalExecutors.BOUNDED);
}
- @Override
- public void onRequestFullScreen(boolean fullScreen) {
- MediaSendFragment sendFragment = (MediaSendFragment) getSupportFragmentManager().findFragmentByTag(TAG_SEND);
- if (sendFragment != null && sendFragment.isVisible()) {
- sendFragment.onRequestFullScreen(fullScreen);
+ private @Nullable MediaSendFragment getMediaSendFragment() {
+ return (MediaSendFragment) getSupportFragmentManager().findFragmentByTag(TAG_SEND);
+ }
+
+ private class ComposeKeyPressedListener implements View.OnKeyListener, View.OnClickListener, TextWatcher, View.OnFocusChangeListener {
+
+ int beforeLength;
+
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER) {
+ if (TextSecurePreferences.isEnterSendsEnabled(getApplicationContext())) {
+ sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
+ sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
+ return true;
+ }
+ }
+ }
+ return false;
}
+
+ @Override
+ public void onClick(View v) {
+ hud.showSoftkey(composeText);
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count,int after) {
+ beforeLength = composeText.getTextTrimmed().length();
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ presentCharactersRemaining();
+ viewModel.onBodyChanged(s);
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before,int count) {}
+
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {}
}
}
diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java b/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java
index 02e85f564a..2d65e37279 100644
--- a/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java
+++ b/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java
@@ -1,80 +1,31 @@
package org.thoughtcrime.securesms.mediasend;
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.PorterDuff;
-import android.graphics.Rect;
+import androidx.lifecycle.ViewModelProviders;
import android.net.Uri;
-import android.os.AsyncTask;
import android.os.Bundle;
-import android.text.Editable;
-import android.text.TextWatcher;
-import android.view.KeyEvent;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.viewpager.widget.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
-import android.view.WindowManager;
-import android.view.inputmethod.EditorInfo;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.view.ContextThemeWrapper;
-import androidx.fragment.app.Fragment;
-import androidx.lifecycle.ViewModelProviders;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.viewpager.widget.ViewPager;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.TransportOption;
-import org.thoughtcrime.securesms.components.ComposeText;
import org.thoughtcrime.securesms.components.ControllableViewPager;
-import org.thoughtcrime.securesms.components.InputAwareLayout;
-import org.thoughtcrime.securesms.components.SendButton;
-import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
-import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
-import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
-import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
-import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
-import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
-import org.thoughtcrime.securesms.logging.Log;
-import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
-import org.thoughtcrime.securesms.mms.GlideApp;
-import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
-import org.thoughtcrime.securesms.scribbles.ImageEditorFragment;
-import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
-import org.thoughtcrime.securesms.util.MediaUtil;
-import org.thoughtcrime.securesms.util.Stopwatch;
-import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
-import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
-import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
-import org.thoughtcrime.securesms.util.views.Stub;
-import org.whispersystems.libsignal.util.guava.Optional;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
-import java.util.concurrent.ExecutionException;
/**
* Allows the user to edit and caption a set of media items before choosing to send them.
*/
-public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGlobalLayoutListener,
- MediaRailAdapter.RailItemListener,
- InputAwareLayout.OnKeyboardShownListener,
- InputAwareLayout.OnKeyboardHiddenListener
-{
+public class MediaSendFragment extends Fragment {
private static final String TAG = MediaSendFragment.class.getSimpleName();
@@ -82,28 +33,12 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
private static final String KEY_TRANSPORT = "transport";
private static final String KEY_LOCALE = "locale";
- private InputAwareLayout hud;
- private View captionAndRail;
- private SendButton sendButton;
- private ComposeText composeText;
- private ViewGroup composeContainer;
- private EmojiEditText captionText;
- private EmojiToggle emojiToggle;
- private Stub emojiDrawer;
- private ViewGroup playbackControlsContainer;
- private TextView charactersLeft;
-
+ private ViewGroup playbackControlsContainer;
private ControllableViewPager fragmentPager;
private MediaSendFragmentPagerAdapter fragmentPagerAdapter;
- private RecyclerView mediaRail;
- private MediaRailAdapter mediaRailAdapter;
- private int visibleHeight;
private MediaSendViewModel viewModel;
- private Controller controller;
- private Locale locale;
- private final Rect visibleBounds = new Rect();
public static MediaSendFragment newInstance(@NonNull Recipient recipient, @NonNull TransportOption transport, @NonNull Locale locale) {
Bundle args = new Bundle();
@@ -116,17 +51,6 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
return fragment;
}
- @Override
- public void onAttach(Context context) {
- super.onAttach(context);
-
- if (!(requireActivity() instanceof Controller)) {
- throw new IllegalStateException("Parent activity must implement controller interface.");
- }
-
- controller = (Controller) requireActivity();
- }
-
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return ThemeUtil.getThemedInflater(requireActivity(), inflater, R.style.TextSecure_DarkTheme)
@@ -137,52 +61,14 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- locale = (Locale) getArguments().getSerializable(KEY_LOCALE);
-
initViewModel();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- hud = view.findViewById(R.id.mediasend_hud);
- captionAndRail = view.findViewById(R.id.mediasend_caption_and_rail);
- sendButton = view.findViewById(R.id.mediasend_send_button);
- composeText = view.findViewById(R.id.mediasend_compose_text);
- composeContainer = view.findViewById(R.id.mediasend_compose_container);
- captionText = view.findViewById(R.id.mediasend_caption);
- emojiToggle = view.findViewById(R.id.mediasend_emoji_toggle);
- emojiDrawer = new Stub<>(view.findViewById(R.id.mediasend_emoji_drawer_stub));
fragmentPager = view.findViewById(R.id.mediasend_pager);
- mediaRail = view.findViewById(R.id.mediasend_media_rail);
playbackControlsContainer = view.findViewById(R.id.mediasend_playback_controls_container);
- charactersLeft = view.findViewById(R.id.mediasend_characters_left);
- View sendButtonBkg = view.findViewById(R.id.mediasend_send_button_bkg);
-
- sendButton.setOnClickListener(v -> {
- if (hud.isKeyboardOpen()) {
- hud.hideSoftkey(composeText, null);
- }
-
- processMedia(fragmentPagerAdapter.getAllMedia(), fragmentPagerAdapter.getSavedState());
- });
-
- sendButton.addOnTransportChangedListener((newTransport, manuallySelected) -> {
- presentCharactersRemaining();
- composeText.setTransport(newTransport);
- sendButtonBkg.getBackground().setColorFilter(newTransport.getBackgroundColor(), PorterDuff.Mode.MULTIPLY);
- sendButtonBkg.getBackground().invalidateSelf();
- });
-
- ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener();
-
- composeText.setOnKeyListener(composeKeyPressedListener);
- composeText.addTextChangedListener(composeKeyPressedListener);
- composeText.setOnClickListener(composeKeyPressedListener);
- composeText.setOnFocusChangeListener(composeKeyPressedListener);
-
- captionText.clearFocus();
- composeText.requestFocus();
fragmentPagerAdapter = new MediaSendFragmentPagerAdapter(getChildFragmentManager());
fragmentPager.setAdapter(fragmentPagerAdapter);
@@ -190,45 +76,6 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
FragmentPageChangeListener pageChangeListener = new FragmentPageChangeListener();
fragmentPager.addOnPageChangeListener(pageChangeListener);
fragmentPager.post(() -> pageChangeListener.onPageSelected(fragmentPager.getCurrentItem()));
-
- mediaRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, true);
- mediaRail.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false));
- mediaRail.setAdapter(mediaRailAdapter);
-
- hud.getRootView().getViewTreeObserver().addOnGlobalLayoutListener(this);
- hud.addOnKeyboardShownListener(this);
- hud.addOnKeyboardHiddenListener(this);
-
- captionText.addTextChangedListener(new SimpleTextWatcher() {
- @Override
- public void onTextChanged(String text) {
- viewModel.onCaptionChanged(text);
- }
- });
-
- TransportOption transportOption = getArguments().getParcelable(KEY_TRANSPORT);
-
- sendButton.setTransport(transportOption);
- sendButton.disableTransport(transportOption.getType() == TransportOption.Type.SMS ? TransportOption.Type.TEXTSECURE : TransportOption.Type.SMS);
-
- composeText.append(viewModel.getBody());
-
- Recipient recipient = Recipient.from(requireContext(), getArguments().getParcelable(KEY_ADDRESS), false);
- String displayName = Optional.fromNullable(recipient.getName())
- .or(Optional.fromNullable(recipient.getProfileName())
- .or(recipient.getAddress().serialize()));
- composeText.setHint(getString(R.string.MediaSendActivity_message_to_s, displayName), null);
- composeText.setOnEditorActionListener((v, actionId, event) -> {
- boolean isSend = actionId == EditorInfo.IME_ACTION_SEND;
- if (isSend) sendButton.performClick();
- return isSend;
- });
-
- if (TextSecurePreferences.isSystemEmojiPreferred(getContext())) {
- emojiToggle.setVisibility(View.GONE);
- } else {
- emojiToggle.setOnClickListener(this::onEmojiToggleClicked);
- }
}
@Override
@@ -237,9 +84,6 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
fragmentPagerAdapter.restoreState(viewModel.getDrawState());
viewModel.onImageEditorStarted();
-
- requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
- requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
}
@Override
@@ -254,82 +98,22 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
viewModel.saveDrawState(fragmentPagerAdapter.getSavedState());
}
- @Override
- public void onGlobalLayout() {
- hud.getRootView().getWindowVisibleDisplayFrame(visibleBounds);
-
- int currentVisibleHeight = visibleBounds.height();
-
- if (currentVisibleHeight != visibleHeight) {
- hud.getLayoutParams().height = currentVisibleHeight;
- hud.layout(visibleBounds.left, visibleBounds.top, visibleBounds.right, visibleBounds.bottom);
- hud.requestLayout();
-
- visibleHeight = currentVisibleHeight;
- }
- }
-
- @Override
- public void onRailItemClicked(int distanceFromActive) {
- viewModel.onPageChanged(fragmentPager.getCurrentItem() + distanceFromActive);
- }
-
- @Override
- public void onRailItemDeleteClicked(int distanceFromActive) {
- viewModel.onMediaItemRemoved(requireContext(), fragmentPager.getCurrentItem() + distanceFromActive);
- }
-
- @Override
- public void onKeyboardShown() {
- if (sendButton.getSelectedTransport().isSms()) {
- mediaRail.setVisibility(View.GONE);
- composeContainer.setVisibility(View.VISIBLE);
- captionText.setVisibility(View.GONE);
- } else {
- if (captionText.hasFocus()) {
- mediaRail.setVisibility(View.VISIBLE);
- composeContainer.setVisibility(View.GONE);
- captionText.setVisibility(View.VISIBLE);
- } else if (composeText.hasFocus()) {
- mediaRail.setVisibility(View.VISIBLE);
- composeContainer.setVisibility(View.VISIBLE);
- captionText.setVisibility(View.GONE);
- } else {
- mediaRail.setVisibility(View.GONE);
- composeContainer.setVisibility(View.VISIBLE);
- captionText.setVisibility(View.GONE);
- }
- }
- }
-
- @Override
- public void onKeyboardHidden() {
- composeContainer.setVisibility(View.VISIBLE);
-
- if (sendButton.getSelectedTransport().isSms()) {
- mediaRail.setVisibility(View.GONE);
- captionText.setVisibility(View.GONE);
- } else {
- mediaRail.setVisibility(View.VISIBLE);
-
- if (!Util.isEmpty(viewModel.getSelectedMedia().getValue()) && viewModel.getSelectedMedia().getValue().size() > 1) {
- captionText.setVisibility(View.VISIBLE);
- }
- }
- }
-
public void onTouchEventsNeeded(boolean needed) {
if (fragmentPager != null) {
fragmentPager.setEnabled(!needed);
}
}
- public boolean handleBackPress() {
- if (hud.isInputOpen()) {
- hud.hideCurrentInput(composeText);
- return true;
- }
- return false;
+ public List getAllMedia() {
+ return fragmentPagerAdapter.getAllMedia();
+ }
+
+ public @NonNull Map getSavedState() {
+ return fragmentPagerAdapter.getSavedState();
+ }
+
+ public int getCurrentImagePosition() {
+ return fragmentPager.getCurrentItem();
}
private void initViewModel() {
@@ -337,27 +121,16 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
viewModel.getSelectedMedia().observe(this, media -> {
if (Util.isEmpty(media)) {
- controller.onNoMediaAvailable();
return;
}
fragmentPagerAdapter.setMedia(media);
-
- mediaRail.setVisibility(sendButton.getSelectedTransport().isSms() ? View.GONE : View.VISIBLE);
- captionText.setVisibility((media.size() > 1 || media.get(0).getCaption().isPresent()) ? View.VISIBLE : View.GONE);
- mediaRailAdapter.setMedia(media);
});
viewModel.getPosition().observe(this, position -> {
if (position == null || position < 0) return;
fragmentPager.setCurrentItem(position, true);
- mediaRailAdapter.setActivePosition(position);
- mediaRail.smoothScrollToPosition(position);
-
- if (fragmentPagerAdapter.getAllMedia().size() > position) {
- captionText.setText(fragmentPagerAdapter.getAllMedia().get(position).getCaption().or(""));
- }
View playbackControls = fragmentPagerAdapter.getPlaybackControls(position);
@@ -370,146 +143,6 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
playbackControlsContainer.removeAllViews();
}
});
-
- viewModel.getBucketId().observe(this, bucketId -> {
- if (bucketId == null) return;
-
- mediaRailAdapter.setAddButtonListener(() -> controller.onAddMediaClicked(bucketId));
- });
- }
-
- private EmojiEditText getActiveInputField() {
- if (captionText.hasFocus()) return captionText;
- else return composeText;
- }
-
-
- private void presentCharactersRemaining() {
- String messageBody = composeText.getTextTrimmed();
- TransportOption transportOption = sendButton.getSelectedTransport();
- CharacterState characterState = transportOption.calculateCharacters(messageBody);
-
- if (characterState.charactersRemaining <= 15 || characterState.messagesSpent > 1) {
- charactersLeft.setText(String.format(locale,
- "%d/%d (%d)",
- characterState.charactersRemaining,
- characterState.maxTotalMessageSize,
- characterState.messagesSpent));
- charactersLeft.setVisibility(View.VISIBLE);
- } else {
- charactersLeft.setVisibility(View.GONE);
- }
- }
-
- private void onEmojiToggleClicked(View v) {
- if (!emojiDrawer.resolved()) {
- emojiDrawer.get().setProviders(0, new EmojiKeyboardProvider(requireContext(), new EmojiKeyboardProvider.EmojiEventListener() {
- @Override
- public void onKeyEvent(KeyEvent keyEvent) {
- getActiveInputField().dispatchKeyEvent(keyEvent);
- }
-
- @Override
- public void onEmojiSelected(String emoji) {
- getActiveInputField().insertEmoji(emoji);
- }
- }));
- emojiToggle.attach(emojiDrawer.get());
- }
-
- if (hud.getCurrentInput() == emojiDrawer.get()) {
- hud.showSoftkey(composeText);
- } else {
- hud.hideSoftkey(composeText, () -> hud.post(() -> hud.show(composeText, emojiDrawer.get())));
- }
- }
-
- @SuppressLint("StaticFieldLeak")
- private void processMedia(@NonNull List mediaList, @NonNull Map savedState) {
- Map modelsToRender = new HashMap<>();
-
- for (Media media : mediaList) {
- Object state = savedState.get(media.getUri());
-
- if (state instanceof ImageEditorFragment.Data) {
- EditorModel model = ((ImageEditorFragment.Data) state).readModel();
- if (model != null && model.isChanged()) {
- modelsToRender.put(media, model);
- }
- }
- }
-
- new AsyncTask>() {
-
- private Stopwatch renderTimer;
- private Runnable progressTimer;
- private AlertDialog dialog;
-
- @Override
- protected void onPreExecute() {
- renderTimer = new Stopwatch("ProcessMedia");
- progressTimer = () -> {
- dialog = new AlertDialog.Builder(new ContextThemeWrapper(requireContext(), R.style.TextSecure_MediaSendProgressDialog))
- .setView(R.layout.progress_dialog)
- .setCancelable(false)
- .create();
- dialog.show();
- dialog.getWindow().setLayout(getResources().getDimensionPixelSize(R.dimen.mediasend_progress_dialog_size),
- getResources().getDimensionPixelSize(R.dimen.mediasend_progress_dialog_size));
- };
- Util.runOnMainDelayed(progressTimer, 250);
- }
-
- @Override
- protected List doInBackground(Void... voids) {
- Context context = requireContext();
- List updatedMedia = new ArrayList<>(mediaList.size());
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-
- for (Media media : mediaList) {
- EditorModel modelToRender = modelsToRender.get(media);
- if (modelToRender != null) {
- Bitmap bitmap = modelToRender.render(context);
- try {
- outputStream.reset();
- bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
-
- Uri uri = BlobProvider.getInstance()
- .forData(outputStream.toByteArray())
- .withMimeType(MediaUtil.IMAGE_JPEG)
- .createForSingleSessionOnDisk(context, e -> Log.w(TAG, "Failed to write to disk.", e));
-
- Media updated = new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), outputStream.size(), media.getBucketId(), media.getCaption());
-
- updatedMedia.add(updated);
- renderTimer.split("item");
- } catch (IOException e) {
- Log.w(TAG, "Failed to render image. Using base image.");
- updatedMedia.add(media);
- } finally {
- bitmap.recycle();
- }
- } else {
- updatedMedia.add(media);
- }
- }
- return updatedMedia;
- }
-
- @Override
- protected void onPostExecute(List media) {
- controller.onSendClicked(media, composeText.getTextTrimmed(), sendButton.getSelectedTransport());
- Util.cancelRunnableOnMain(progressTimer);
- if (dialog != null) {
- dialog.dismiss();
- }
- renderTimer.stop(TAG);
- }
- }.execute();
- }
-
- public void onRequestFullScreen(boolean fullScreen) {
- captionAndRail.setVisibility(fullScreen ? View.GONE : View.VISIBLE);
}
private class FragmentPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
@@ -518,51 +151,4 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
viewModel.onPageChanged(position);
}
}
-
- private class ComposeKeyPressedListener implements View.OnKeyListener, View.OnClickListener, TextWatcher, View.OnFocusChangeListener {
-
- int beforeLength;
-
- @Override
- public boolean onKey(View v, int keyCode, KeyEvent event) {
- if (event.getAction() == KeyEvent.ACTION_DOWN) {
- if (keyCode == KeyEvent.KEYCODE_ENTER) {
- if (TextSecurePreferences.isEnterSendsEnabled(requireContext())) {
- sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
- sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
- return true;
- }
- }
- }
- return false;
- }
-
- @Override
- public void onClick(View v) {
- hud.showSoftkey(composeText);
- }
-
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count,int after) {
- beforeLength = composeText.getTextTrimmed().length();
- }
-
- @Override
- public void afterTextChanged(Editable s) {
- presentCharactersRemaining();
- viewModel.onBodyChanged(s);
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before,int count) {}
-
- @Override
- public void onFocusChange(View v, boolean hasFocus) {}
- }
-
- public interface Controller {
- void onAddMediaClicked(@NonNull String bucketId);
- void onSendClicked(@NonNull List media, @NonNull String body, @NonNull TransportOption transport);
- void onNoMediaAvailable();
- }
}
diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java b/src/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java
index ac045b2cee..55d301f353 100644
--- a/src/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java
+++ b/src/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java
@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.TransportOption;
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.util.MediaUtil;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.Util;
@@ -41,52 +42,69 @@ class MediaSendViewModel extends ViewModel {
private final MediaRepository repository;
private final MutableLiveData> selectedMedia;
private final MutableLiveData> bucketMedia;
+ private final MutableLiveData> mostRecentMedia;
private final MutableLiveData position;
private final MutableLiveData bucketId;
private final MutableLiveData> folders;
- private final MutableLiveData countButtonState;
- private final MutableLiveData cameraButtonVisibility;
+ private final MutableLiveData hudState;
private final SingleLiveEvent error;
private final Map savedDrawState;
- private MediaConstraints mediaConstraints;
- private CharSequence body;
- private CountButtonState.Visibility countButtonVisibility;
- private boolean sentMedia;
- private Optional lastImageCapture;
- private int maxSelection;
+ private MediaConstraints mediaConstraints;
+ private CharSequence body;
+ private boolean sentMedia;
+ private int maxSelection;
+ private Page page;
+ private boolean isSms;
+ private boolean isNoteToSelf;
+ private Optional lastCameraCapture;
+
+ private boolean hudVisible;
+ private boolean composeVisible;
+ private boolean captionVisible;
+ private ButtonState buttonState;
+ private RailState railState;
+ private TimerState timerState;
+
private MediaSendViewModel(@NonNull Application application, @NonNull MediaRepository repository) {
this.application = application;
this.repository = repository;
this.selectedMedia = new MutableLiveData<>();
this.bucketMedia = new MutableLiveData<>();
+ this.mostRecentMedia = new MutableLiveData<>();
this.position = new MutableLiveData<>();
this.bucketId = new MutableLiveData<>();
this.folders = new MutableLiveData<>();
- this.countButtonState = new MutableLiveData<>();
- this.cameraButtonVisibility = new MutableLiveData<>();
+ this.hudState = new MutableLiveData<>();
this.error = new SingleLiveEvent<>();
this.savedDrawState = new HashMap<>();
- this.countButtonVisibility = CountButtonState.Visibility.FORCED_OFF;
- this.lastImageCapture = Optional.absent();
+ this.lastCameraCapture = Optional.absent();
this.body = "";
+ this.buttonState = ButtonState.GONE;
+ this.railState = RailState.GONE;
+ this.timerState = TimerState.GONE;
+ this.page = Page.UNKNOWN;
position.setValue(-1);
- countButtonState.setValue(new CountButtonState(0, countButtonVisibility));
- cameraButtonVisibility.setValue(false);
}
void setTransport(@NonNull TransportOption transport) {
if (transport.isSms()) {
+ isSms = true;
maxSelection = MAX_SMS;
mediaConstraints = MediaConstraints.getMmsMediaConstraints(transport.getSimSubscriptionId().or(-1));
} else {
+ isSms = false;
maxSelection = MAX_PUSH;
mediaConstraints = MediaConstraints.getPushMediaConstraints();
}
}
+ void setRecipient(@NonNull Recipient recipient) {
+ isNoteToSelf = recipient.isLocalNumber();
+ }
+
void onSelectedMediaChanged(@NonNull Context context, @NonNull List newMedia) {
repository.getPopulatedMedia(context, newMedia, populatedMedia -> {
Util.runOnMain(() -> {
@@ -113,11 +131,19 @@ class MediaSendViewModel extends ViewModel {
bucketId.setValue(computedId);
} else {
bucketId.setValue(Media.ALL_MEDIA_BUCKET_ID);
- countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
}
- selectedMedia.setValue(filteredMedia);
- countButtonState.setValue(new CountButtonState(filteredMedia.size(), countButtonVisibility));
+ if (page == Page.EDITOR && filteredMedia.isEmpty()) {
+ error.postValue(Error.NO_ITEMS);
+ } else if (filteredMedia.isEmpty()) {
+ hudVisible = false;
+ selectedMedia.setValue(filteredMedia);
+ hudState.setValue(buildHudState());
+ } else {
+ hudVisible = true;
+ selectedMedia.setValue(filteredMedia);
+ hudState.setValue(buildHudState());
+ }
});
});
}
@@ -134,41 +160,131 @@ class MediaSendViewModel extends ViewModel {
bucketId.setValue(filteredMedia.get(0).getBucketId().or(Media.ALL_MEDIA_BUCKET_ID));
}
- countButtonVisibility = CountButtonState.Visibility.FORCED_OFF;
-
selectedMedia.setValue(filteredMedia);
- countButtonState.setValue(new CountButtonState(filteredMedia.size(), countButtonVisibility));
});
});
}
void onMultiSelectStarted() {
- countButtonVisibility = CountButtonState.Visibility.FORCED_ON;
- countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
+ hudVisible = true;
+ composeVisible = false;
+ captionVisible = false;
+ buttonState = ButtonState.COUNT;
+ railState = RailState.VIEWABLE;
+ timerState = TimerState.GONE;
+
+ hudState.setValue(buildHudState());
}
void onImageEditorStarted() {
- countButtonVisibility = CountButtonState.Visibility.FORCED_OFF;
- countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
- cameraButtonVisibility.setValue(false);
+ page = Page.EDITOR;
+ hudVisible = true;
+ composeVisible = timerState != TimerState.ENABLED;
+ captionVisible = getSelectedMediaOrDefault().size() > 1 || (getSelectedMediaOrDefault().size() > 0 && getSelectedMediaOrDefault().get(0).getCaption().isPresent());
+ buttonState = ButtonState.SEND;
+ railState = !isSms ? RailState.INTERACTIVE : RailState.GONE;
+
+ hudState.setValue(buildHudState());
}
void onCameraStarted() {
- countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
- countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
- cameraButtonVisibility.setValue(false);
+ Page previous = page;
+
+ page = Page.CAMERA;
+ hudVisible = false;
+ timerState = TimerState.GONE;
+ buttonState = ButtonState.COUNT;
+
+ List selected = getSelectedMediaOrDefault();
+
+ if (previous == Page.EDITOR && lastCameraCapture.isPresent() && selected.contains(lastCameraCapture.get()) && selected.size() == 1) {
+ selected.remove(lastCameraCapture.get());
+ selectedMedia.setValue(selected);
+ BlobProvider.getInstance().delete(application, lastCameraCapture.get().getUri());
+ }
+
+ hudState.setValue(buildHudState());
}
void onItemPickerStarted() {
- countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
- countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
- cameraButtonVisibility.setValue(true);
+ page = Page.ITEM_PICKER;
+ hudVisible = true;
+ composeVisible = false;
+ captionVisible = false;
+ buttonState = ButtonState.COUNT;
+ timerState = TimerState.GONE;
+ railState = getSelectedMediaOrDefault().isEmpty() ? RailState.GONE : RailState.VIEWABLE;
+
+ lastCameraCapture = Optional.absent();
+
+ hudState.setValue(buildHudState());
}
void onFolderPickerStarted() {
- countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
- countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
- cameraButtonVisibility.setValue(true);
+ page = Page.FOLDER_PICKER;
+ hudVisible = true;
+ composeVisible = false;
+ captionVisible = false;
+ buttonState = ButtonState.COUNT;
+ timerState = TimerState.GONE;
+ railState = getSelectedMediaOrDefault().isEmpty() ? RailState.GONE : RailState.VIEWABLE;
+
+ lastCameraCapture = Optional.absent();
+
+ hudState.setValue(buildHudState());
+ }
+
+ void onTimerButtonToggled() {
+ hudVisible = true;
+ timerState = (timerState == TimerState.ENABLED) ? TimerState.DISABLED : TimerState.ENABLED;
+ composeVisible = (timerState != TimerState.ENABLED);
+
+ hudState.setValue(buildHudState());
+ }
+
+ void onKeyboardHidden(boolean isSms) {
+ if (page != Page.EDITOR) return;
+
+ composeVisible = (timerState != TimerState.ENABLED);
+ buttonState = ButtonState.SEND;
+
+ if (isSms) {
+ railState = RailState.GONE;
+ captionVisible = false;
+ } else {
+ railState = RailState.INTERACTIVE;
+
+ if (getSelectedMediaOrDefault().size() > 1 || (getSelectedMediaOrDefault().size() > 0 && getSelectedMediaOrDefault().get(0).getCaption().isPresent())) {
+ captionVisible = true;
+ }
+ }
+
+ hudState.setValue(buildHudState());
+ }
+
+ void onKeyboardShown(boolean isComposeFocused, boolean isCaptionFocused, boolean isSms) {
+ if (page != Page.EDITOR) return;
+
+ if (isSms) {
+ railState = RailState.GONE;
+ composeVisible = (timerState == TimerState.GONE);
+ captionVisible = false;
+ buttonState = ButtonState.SEND;
+ } else {
+ if (isCaptionFocused) {
+ railState = RailState.INTERACTIVE;
+ composeVisible = false;
+ captionVisible = true;
+ buttonState = ButtonState.GONE;
+ } else if (isComposeFocused) {
+ railState = RailState.INTERACTIVE;
+ composeVisible = (timerState != TimerState.ENABLED);
+ captionVisible = false;
+ buttonState = ButtonState.SEND;
+ }
+ }
+
+ hudState.setValue(buildHudState());
}
void onBodyChanged(@NonNull CharSequence body) {
@@ -201,10 +317,22 @@ class MediaSendViewModel extends ViewModel {
BlobProvider.getInstance().delete(context, removed.getUri());
}
- selectedMedia.setValue(selectedMedia.getValue());
+ if (page == Page.EDITOR && getSelectedMediaOrDefault().isEmpty()) {
+ error.setValue(Error.NO_ITEMS);
+ } else {
+ selectedMedia.setValue(selectedMedia.getValue());
+ }
+
+ if (getSelectedMediaOrDefault().size() > 0) {
+ this.position.setValue(Math.min(position, getSelectedMediaOrDefault().size() - 1));
+ }
+
+ hudState.setValue(buildHudState());
}
void onImageCaptured(@NonNull Media media) {
+ lastCameraCapture = Optional.of(media);
+
List selected = selectedMedia.getValue();
if (selected == null) {
@@ -216,40 +344,32 @@ class MediaSendViewModel extends ViewModel {
return;
}
- lastImageCapture = Optional.of(media);
-
selected.add(media);
selectedMedia.setValue(selected);
position.setValue(selected.size() - 1);
bucketId.setValue(Media.ALL_MEDIA_BUCKET_ID);
-
- if (selected.size() == 1) {
- countButtonVisibility = CountButtonState.Visibility.FORCED_OFF;
- } else {
- countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
- }
-
- countButtonState.setValue(new CountButtonState(selected.size(), countButtonVisibility));
}
void onImageCaptureUndo(@NonNull Context context) {
List selected = getSelectedMediaOrDefault();
- if (lastImageCapture.isPresent() && selected.contains(lastImageCapture.get()) && selected.size() == 1) {
- selected.remove(lastImageCapture.get());
+ if (lastCameraCapture.isPresent() && selected.contains(lastCameraCapture.get()) && selected.size() == 1) {
+ selected.remove(lastCameraCapture.get());
selectedMedia.setValue(selected);
- countButtonState.setValue(new CountButtonState(selected.size(), countButtonVisibility));
- BlobProvider.getInstance().delete(context, lastImageCapture.get().getUri());
+ BlobProvider.getInstance().delete(context, lastCameraCapture.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);
}
}
+ void onCameraControlsInitialized() {
+ repository.getMostRecentItem(application, mostRecentMedia::postValue);
+ }
+
void saveDrawState(@NonNull Map state) {
savedDrawState.clear();
savedDrawState.putAll(state);
@@ -277,12 +397,8 @@ class MediaSendViewModel extends ViewModel {
return folders;
}
- @NonNull LiveData getCountButtonState() {
- return countButtonState;
- }
-
- @NonNull LiveData getCameraButtonVisibility() {
- return cameraButtonVisibility;
+ @NonNull LiveData> getMostRecentMediaItem(@NonNull Context context) {
+ return mostRecentMedia;
}
@NonNull CharSequence getBody() {
@@ -301,10 +417,18 @@ class MediaSendViewModel extends ViewModel {
return error;
}
+ @NonNull LiveData getHudState() {
+ return hudState;
+ }
+
int getMaxSelection() {
return maxSelection;
}
+ long getRevealDuration() {
+ return 0;
+ }
+
private @NonNull List getSelectedMediaOrDefault() {
return selectedMedia.getValue() == null ? Collections.emptyList()
: selectedMedia.getValue();
@@ -322,44 +446,102 @@ class MediaSendViewModel extends ViewModel {
}
+ private HudState buildHudState() {
+ List selectedMedia = getSelectedMediaOrDefault();
+ int selectionCount = selectedMedia.size();
+ ButtonState updatedButtonState = buttonState == ButtonState.COUNT && selectionCount == 0 ? ButtonState.GONE : buttonState;
+ boolean updatdCaptionVisible = captionVisible && (selectedMedia.size() > 1 || (selectedMedia.size() > 0 && selectedMedia.get(0).getCaption().isPresent()));
+
+ return new HudState(hudVisible, composeVisible, updatdCaptionVisible, selectionCount, updatedButtonState, railState, timerState);
+ }
+
+ private void clearPersistedMedia() {
+ Stream.of(getSelectedMediaOrDefault())
+ .map(Media::getUri)
+ .filter(BlobProvider::isAuthority)
+ .forEach(uri -> BlobProvider.getInstance().delete(application.getApplicationContext(), uri));
+ }
+
@Override
protected void onCleared() {
if (!sentMedia) {
- Stream.of(getSelectedMediaOrDefault())
- .map(Media::getUri)
- .filter(BlobProvider::isAuthority)
- .forEach(uri -> BlobProvider.getInstance().delete(application.getApplicationContext(), uri));
+ clearPersistedMedia();
}
}
enum Error {
- ITEM_TOO_LARGE, TOO_MANY_ITEMS
+ ITEM_TOO_LARGE, TOO_MANY_ITEMS, NO_ITEMS
}
- static class CountButtonState {
- private final int count;
- private final Visibility visibility;
+ enum Page {
+ CAMERA, ITEM_PICKER, FOLDER_PICKER, EDITOR, UNKNOWN
+ }
- private CountButtonState(int count, @NonNull Visibility visibility) {
- this.count = count;
- this.visibility = visibility;
+ enum ButtonState {
+ COUNT, SEND, GONE
+ }
+
+ enum RailState {
+ INTERACTIVE, VIEWABLE, GONE
+ }
+
+ enum TimerState {
+ ENABLED, DISABLED, GONE
+ }
+
+ static class HudState {
+
+ private final boolean hudVisible;
+ private final boolean composeVisible;
+ private final boolean captionVisible;
+ private final int selectionCount;
+ private final ButtonState buttonState;
+ private final RailState railState;
+ private final TimerState timerState;
+
+ HudState(boolean hudVisible,
+ boolean composeVisible,
+ boolean captionVisible,
+ int selectionCount,
+ @NonNull ButtonState buttonState,
+ @NonNull RailState railState,
+ @NonNull TimerState timerState)
+ {
+ this.hudVisible = hudVisible;
+ this.composeVisible = composeVisible;
+ this.captionVisible = captionVisible;
+ this.selectionCount = selectionCount;
+ this.buttonState = buttonState;
+ this.railState = railState;
+ this.timerState = timerState;
}
- int getCount() {
- return count;
+ public boolean isHudVisible() {
+ return hudVisible;
}
- boolean isVisible() {
- switch (visibility) {
- case FORCED_ON: return true;
- case FORCED_OFF: return false;
- case CONDITIONAL: return count > 0;
- default: return false;
- }
+ public boolean isComposeVisible() {
+ return hudVisible && composeVisible;
}
- enum Visibility {
- CONDITIONAL, FORCED_ON, FORCED_OFF
+ public boolean isCaptionVisible() {
+ return hudVisible && captionVisible;
+ }
+
+ public int getSelectionCount() {
+ return selectionCount;
+ }
+
+ public @NonNull ButtonState getButtonState() {
+ return buttonState;
+ }
+
+ public @NonNull RailState getRailState() {
+ return hudVisible ? railState : RailState.GONE;
+ }
+
+ public @NonNull TimerState getTimerState() {
+ return hudVisible ? timerState : TimerState.GONE;
}
}
diff --git a/src/org/thoughtcrime/securesms/util/concurrent/SimpleTask.java b/src/org/thoughtcrime/securesms/util/concurrent/SimpleTask.java
index 5cd24618f9..91cfe7f877 100644
--- a/src/org/thoughtcrime/securesms/util/concurrent/SimpleTask.java
+++ b/src/org/thoughtcrime/securesms/util/concurrent/SimpleTask.java
@@ -20,7 +20,7 @@ public class SimpleTask {
return;
}
- AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
+ SignalExecutors.BOUNDED.execute(() -> {
final E result = backgroundTask.run();
if (isValid(lifecycle)) {
@@ -38,7 +38,7 @@ public class SimpleTask {
* the main thread. Essentially {@link AsyncTask}, but lambda-compatible.
*/
public static void run(@NonNull BackgroundTask backgroundTask, @NonNull ForegroundTask foregroundTask) {
- AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
+ SignalExecutors.BOUNDED.execute(() -> {
final E result = backgroundTask.run();
Util.runOnMain(() -> foregroundTask.run(result));
});