Updated media send UI.

This commit is contained in:
Greyson Parrelli
2019-03-14 17:01:23 -07:00
parent d8a56be5e8
commit 64cf032181
142 changed files with 651 additions and 347 deletions

View File

@@ -257,10 +257,10 @@ public class AttachmentTypeSelector extends PopupWindow {
private class RecentPhotoSelectedListener implements RecentPhotoViewRail.OnItemClickedListener {
@Override
public void onItemClicked(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height) {
public void onItemClicked(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height, long size) {
animateWindowOutTranslate(getContentView());
if (listener != null) listener.onQuickAttachment(uri, mimeType, bucketId, dateTaken, width, height);
if (listener != null) listener.onQuickAttachment(uri, mimeType, bucketId, dateTaken, width, height, size);
}
}
@@ -290,7 +290,7 @@ public class AttachmentTypeSelector extends PopupWindow {
public interface AttachmentClickedListener {
void onClick(int type);
void onQuickAttachment(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height);
void onQuickAttachment(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height, long size);
}
}

View File

@@ -110,6 +110,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
String mimeType = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.MIME_TYPE));
String bucketId = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.BUCKET_ID));
int orientation = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.ORIENTATION));
long size = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.SIZE));
int width = Build.VERSION.SDK_INT >= 16 ? cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation))) : 0;
int height = Build.VERSION.SDK_INT >= 16 ? cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation))) : 0;
@@ -124,7 +125,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
.into(viewHolder.imageView);
viewHolder.imageView.setOnClickListener(v -> {
if (clickedListener != null) clickedListener.onItemClicked(uri, mimeType, bucketId, dateTaken, width, height);
if (clickedListener != null) clickedListener.onItemClicked(uri, mimeType, bucketId, dateTaken, width, height, size);
});
}
@@ -160,6 +161,6 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
}
public interface OnItemClickedListener {
void onItemClicked(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height);
void onItemClicked(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height, long size);
}
}

View File

@@ -2365,10 +2365,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
@Override
public void onQuickAttachment(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height) {
public void onQuickAttachment(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height, long size) {
linkPreviewViewModel.onUserCancel();
// TODO: Carry over size?
Media media = new Media(uri, mimeType, dateTaken, width, height, 0, Optional.of(Media.ALL_MEDIA_BUCKET_ID), Optional.absent());
Media media = new Media(uri, mimeType, dateTaken, width, height, size, Optional.of(Media.ALL_MEDIA_BUCKET_ID), Optional.absent());
startActivityForResult(MediaSendActivity.buildEditorIntent(ConversationActivity.this, Collections.singletonList(media), recipient, composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER);
}
}

View File

@@ -21,7 +21,8 @@ public class RecentPhotosLoader extends CursorLoader {
MediaStore.Images.ImageColumns.DATE_MODIFIED,
MediaStore.Images.ImageColumns.ORIENTATION,
MediaStore.Images.ImageColumns.MIME_TYPE,
MediaStore.Images.ImageColumns.BUCKET_ID
MediaStore.Images.ImageColumns.BUCKET_ID,
MediaStore.Images.ImageColumns.SIZE
};
private static final String[] PROJECTION_16 = new String[] {
@@ -31,6 +32,7 @@ public class RecentPhotosLoader extends CursorLoader {
MediaStore.Images.ImageColumns.ORIENTATION,
MediaStore.Images.ImageColumns.MIME_TYPE,
MediaStore.Images.ImageColumns.BUCKET_ID,
MediaStore.Images.ImageColumns.SIZE,
MediaStore.Images.ImageColumns.WIDTH,
MediaStore.Images.ImageColumns.HEIGHT
};

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.mediapreview;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
@@ -8,6 +9,7 @@ import android.view.ViewGroup;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.util.StableIdGenerator;
@@ -17,19 +19,23 @@ import java.util.List;
public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.MediaRailViewHolder> {
private static final int TYPE_MEDIA = 1;
private static final int TYPE_BUTTON = 2;
private final GlideRequests glideRequests;
private final List<Media> media;
private final RailItemListener listener;
private final boolean deleteEnabled;
private final boolean editable;
private final StableIdGenerator<Media> stableIdGenerator;
private RailItemAddListener addListener;
private int activePosition;
public MediaRailAdapter(@NonNull GlideRequests glideRequests, @NonNull RailItemListener listener, boolean deleteEnabled) {
public MediaRailAdapter(@NonNull GlideRequests glideRequests, @NonNull RailItemListener listener, boolean editable) {
this.glideRequests = glideRequests;
this.media = new ArrayList<>();
this.listener = listener;
this.deleteEnabled = deleteEnabled;
this.editable = editable;
this.stableIdGenerator = new StableIdGenerator<>();
setHasStableIds(true);
@@ -37,13 +43,38 @@ public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.Medi
@NonNull
@Override
public MediaRailViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
return new MediaRailViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.media_preview_album_rail_item, viewGroup, false));
public MediaRailViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int type) {
switch (type) {
case TYPE_MEDIA:
return new MediaViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.mediarail_media_item, viewGroup, false));
case TYPE_BUTTON:
return new ButtonViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.mediarail_button_item, viewGroup, false));
default:
throw new UnsupportedOperationException("Unsupported view type: " + type);
}
}
@Override
public void onBindViewHolder(@NonNull MediaRailViewHolder mediaRailViewHolder, int i) {
mediaRailViewHolder.bind(media.get(i), i == activePosition, glideRequests, listener, i - activePosition, deleteEnabled);
public void onBindViewHolder(@NonNull MediaRailViewHolder viewHolder, int i) {
switch (getItemViewType(i)) {
case TYPE_MEDIA:
((MediaViewHolder) viewHolder).bind(media.get(i), i == activePosition, glideRequests, listener, i - activePosition, editable);
break;
case TYPE_BUTTON:
((ButtonViewHolder) viewHolder).bind(addListener);
break;
default:
throw new UnsupportedOperationException("Unsupported view type: " + getItemViewType(i));
}
}
@Override
public int getItemViewType(int position) {
if (editable && position == getItemCount() - 1) {
return TYPE_BUTTON;
} else {
return TYPE_MEDIA;
}
}
@Override
@@ -53,12 +84,19 @@ public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.Medi
@Override
public int getItemCount() {
return media.size();
return editable ? media.size() + 1 : media.size();
}
@Override
public long getItemId(int position) {
return stableIdGenerator.getId(media.get(position));
switch (getItemViewType(position)) {
case TYPE_MEDIA:
return stableIdGenerator.getId(media.get(position));
case TYPE_BUTTON:
return Long.MAX_VALUE;
default:
throw new UnsupportedOperationException("Unsupported view type: " + getItemViewType(position));
}
}
public void setMedia(@NonNull List<Media> media) {
@@ -79,25 +117,45 @@ public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.Medi
notifyDataSetChanged();
}
static class MediaRailViewHolder extends RecyclerView.ViewHolder {
public void setAddButtonListener(@Nullable RailItemAddListener addListener) {
this.addListener = addListener;
notifyDataSetChanged();
}
static abstract class MediaRailViewHolder extends RecyclerView.ViewHolder {
public MediaRailViewHolder(@NonNull View itemView) {
super(itemView);
}
abstract void recycle();
}
static class MediaViewHolder extends MediaRailViewHolder {
private final ThumbnailView image;
private final View outline;
private final View deleteButton;
private final View captionIndicator;
MediaRailViewHolder(@NonNull View itemView) {
MediaViewHolder(@NonNull View itemView) {
super(itemView);
image = itemView.findViewById(R.id.rail_item_image);
deleteButton = itemView.findViewById(R.id.rail_item_delete);
image = itemView.findViewById(R.id.rail_item_image);
outline = itemView.findViewById(R.id.rail_item_outline);
deleteButton = itemView.findViewById(R.id.rail_item_delete);
captionIndicator = itemView.findViewById(R.id.rail_item_caption);
}
void bind(@NonNull Media media, boolean isActive, @NonNull GlideRequests glideRequests,
@NonNull RailItemListener railItemListener, int distanceFromActive, boolean deleteEnabled)
@NonNull RailItemListener railItemListener, int distanceFromActive, boolean editable)
{
image.setImageResource(glideRequests, media.getUri());
image.setBackgroundResource(isActive ? R.drawable.media_rail_item_background : 0);
image.setOnClickListener(v -> railItemListener.onRailItemClicked(distanceFromActive));
if (deleteEnabled && isActive) {
outline.setVisibility(isActive ? View.VISIBLE : View.GONE);
captionIndicator.setVisibility(media.getCaption().isPresent() ? View.VISIBLE : View.GONE);
if (editable && isActive) {
deleteButton.setVisibility(View.VISIBLE);
deleteButton.setOnClickListener(v -> railItemListener.onRailItemDeleteClicked(distanceFromActive));
} else {
@@ -111,8 +169,30 @@ public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.Medi
}
}
static class ButtonViewHolder extends MediaRailViewHolder {
public ButtonViewHolder(@NonNull View itemView) {
super(itemView);
}
void bind(@Nullable RailItemAddListener addListener) {
if (addListener != null) {
itemView.setOnClickListener(v -> addListener.onRailItemAddClicked());
}
}
@Override
void recycle() {
itemView.setOnClickListener(null);
}
}
public interface RailItemListener {
void onRailItemClicked(int distanceFromActive);
void onRailItemDeleteClicked(int distanceFromActive);
}
public interface RailItemAddListener {
void onRailItemAddClicked();
}
}

View File

@@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.camera;
package org.thoughtcrime.securesms.mediasend;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
@@ -11,21 +11,21 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class Camera1Controller {
class Camera1Controller {
private static final String TAG = Camera1Controller.class.getSimpleName();
private final int screenWidth;
private final int screenHeight;
private final int screenWidth;
private final int screenHeight;
private final OrderEnforcer<Stage> enforcer;
private final EventListener eventListener;
private Camera camera;
private int cameraId;
private OrderEnforcer<Stage> enforcer;
private EventListener eventListener;
private SurfaceTexture previewSurface;
private int screenRotation;
public Camera1Controller(int preferredDirection, int screenWidth, int screenHeight, @NonNull EventListener eventListener) {
Camera1Controller(int preferredDirection, int screenWidth, int screenHeight, @NonNull EventListener eventListener) {
this.eventListener = eventListener;
this.enforcer = new OrderEnforcer<>(Stage.INITIALIZED, Stage.PREVIEW_STARTED);
this.cameraId = Camera.getNumberOfCameras() > 1 ? preferredDirection : Camera.CameraInfo.CAMERA_FACING_BACK;
@@ -33,10 +33,11 @@ public class Camera1Controller {
this.screenHeight = screenHeight;
}
public void initialize() {
void initialize() {
Log.d(TAG, "initialize()");
if (Camera.getNumberOfCameras() <= 0) {
Log.w(TAG, "Device doesn't have any cameras.");
onCameraUnavailable();
return;
}
@@ -44,11 +45,13 @@ public class Camera1Controller {
try {
camera = Camera.open(cameraId);
} catch (Exception e) {
Log.w(TAG, "Failed to open camera.", e);
onCameraUnavailable();
return;
}
if (camera == null) {
Log.w(TAG, "Null camera instance.");
onCameraUnavailable();
return;
}
@@ -80,9 +83,9 @@ public class Camera1Controller {
eventListener.onPropertiesAvailable(getProperties());
}
public void release() {
void release() {
Log.d(TAG, "release() called");
enforcer.run(Stage.PREVIEW_STARTED, () -> {
enforcer.run(Stage.INITIALIZED, () -> {
Log.d(TAG, "release() executing");
previewSurface = null;
camera.stopPreview();
@@ -91,7 +94,7 @@ public class Camera1Controller {
});
}
public void linkSurface(@NonNull SurfaceTexture surfaceTexture) {
void linkSurface(@NonNull SurfaceTexture surfaceTexture) {
Log.d(TAG, "linkSurface() called");
enforcer.run(Stage.INITIALIZED, () -> {
try {
@@ -108,7 +111,7 @@ public class Camera1Controller {
});
}
public void capture(@NonNull CaptureCallback callback) {
void capture(@NonNull CaptureCallback callback) {
enforcer.run(Stage.PREVIEW_STARTED, () -> {
camera.takePicture(null, null, null, (data, camera) -> {
callback.onCaptureAvailable(data, cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT);
@@ -116,7 +119,7 @@ public class Camera1Controller {
});
}
public int flip() {
int flip() {
Log.d(TAG, "flip()");
SurfaceTexture surfaceTexture = previewSurface;
cameraId = (cameraId == Camera.CameraInfo.CAMERA_FACING_BACK) ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK;
@@ -129,7 +132,7 @@ public class Camera1Controller {
return cameraId;
}
public void setScreenRotation(int screenRotation) {
void setScreenRotation(int screenRotation) {
Log.d(TAG, "setScreenRotation(" + screenRotation + ") called");
enforcer.run(Stage.PREVIEW_STARTED, () -> {
Log.d(TAG, "setScreenRotation(" + screenRotation + ") executing");
@@ -221,7 +224,7 @@ public class Camera1Controller {
private final int previewWidth;
private final int previewHeight;
public Properties(int cameraCount, int previewWidth, int previewHeight) {
Properties(int cameraCount, int previewWidth, int previewHeight) {
this.cameraCount = cameraCount;
this.previewWidth = previewWidth;
this.previewHeight = previewHeight;
@@ -231,11 +234,11 @@ public class Camera1Controller {
return cameraCount;
}
public int getPreviewWidth() {
int getPreviewWidth() {
return previewWidth;
}
public int getPreviewHeight() {
int getPreviewHeight() {
return previewHeight;
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.camera;
package org.thoughtcrime.securesms.mediasend;
import android.annotation.SuppressLint;
import android.arch.lifecycle.ViewModelProviders;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Matrix;
@@ -21,8 +22,11 @@ import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.Button;
import android.widget.ImageButton;
@@ -55,6 +59,7 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
private Controller controller;
private OrderEnforcer<Stage> orderEnforcer;
private Camera1Controller.Properties properties;
private MediaSendViewModel viewModel;
public static Camera1Fragment newInstance() {
return new Camera1Fragment();
@@ -76,6 +81,7 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
controller = (Controller) getActivity();
camera = new Camera1Controller(TextSecurePreferences.getDirectCaptureCameraId(getContext()), displaySize.x, displaySize.y, this);
orderEnforcer = new OrderEnforcer<>(Stage.SURFACE_AVAILABLE, Stage.CAMERA_PROPERTIES_AVAILABLE);
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
}
@Nullable
@@ -103,11 +109,22 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
@Override
public void onResume() {
super.onResume();
viewModel.onCameraStarted();
camera.initialize();
if (cameraPreview.isAvailable()) {
orderEnforcer.markCompleted(Stage.SURFACE_AVAILABLE);
}
if (properties != null) {
orderEnforcer.markCompleted(Stage.CAMERA_PROPERTIES_AVAILABLE);
}
orderEnforcer.run(Stage.SURFACE_AVAILABLE, () -> {
camera.linkSurface(cameraPreview.getSurfaceTexture());
camera.setScreenRotation(controller.getDisplayRotation());
});
orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, this::updatePreviewScale);
requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
@@ -118,6 +135,7 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
public void onPause() {
super.onPause();
camera.release();
orderEnforcer.reset();
}
@Override
@@ -128,6 +146,7 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
Log.d(TAG, "onSurfaceTextureAvailable");
orderEnforcer.markCompleted(Stage.SURFACE_AVAILABLE);
}
@@ -159,10 +178,6 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
controller.onCameraError();
}
public void reset() {
orderEnforcer.reset();
}
@SuppressLint("ClickableViewAccessibility")
private void initControls() {
flipButton = getView().findViewById(R.id.camera_flip_button);
@@ -193,14 +208,14 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, () -> {
if (properties.getCameraCount() > 1) {
flipButton.setVisibility(properties.getCameraCount() > 1 ? View.VISIBLE : View.GONE);
flipButton.setImageResource(TextSecurePreferences.getDirectCaptureCameraId(getContext()) == Camera.CameraInfo.CAMERA_FACING_BACK ? R.drawable.ic_camera_front
: R.drawable.ic_camera_rear);
flipButton.setOnClickListener(v -> {
int newCameraId = camera.flip();
flipButton.setImageResource(newCameraId == Camera.CameraInfo.CAMERA_FACING_BACK ? R.drawable.ic_camera_front
: R.drawable.ic_camera_rear);
TextSecurePreferences.setDirectCaptureCameraId(getContext(), newCameraId);
Animation animation = new RotateAnimation(0, -180, RotateAnimation.RELATIVE_TO_SELF, 0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f);
animation.setDuration(200);
animation.setInterpolator(new DecelerateInterpolator());
flipButton.startAnimation(animation);
});
} else {
flipButton.setVisibility(View.GONE);
@@ -209,7 +224,7 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
}
private void onCaptureClicked() {
reset();
orderEnforcer.reset();
Stopwatch fastCaptureTimer = new Stopwatch("Capture");

View File

@@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.camera;
package org.thoughtcrime.securesms.mediasend;
import android.graphics.Bitmap;
import android.graphics.Canvas;

View File

@@ -93,6 +93,7 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
public void onResume() {
super.onResume();
viewModel.onFolderPickerStarted();
requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}

View File

@@ -4,33 +4,27 @@ import android.arch.lifecycle.ViewModelProviders;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Point;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -119,25 +113,27 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
public void onResume() {
super.onResume();
viewModel.onItemPickerStarted();
requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
requireActivity().getMenuInflater().inflate(R.menu.mediapicker_default, menu);
MenuItem beginSelectionButton = menu.findItem(R.id.mediapicker_menu_add);
beginSelectionButton.setVisible(!viewModel.getCountButtonState().getValue().getVisibility());
if (viewModel.getCountButtonState().getValue() != null && viewModel.getCountButtonState().getValue().isVisible()) {
requireActivity().getMenuInflater().inflate(R.menu.mediapicker_multiselect, menu);
} else {
requireActivity().getMenuInflater().inflate(R.menu.mediapicker_default, menu);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.mediapicker_menu_add) {
adapter.setForcedMultiSelect(true);
viewModel.onMultiSelectStarted();
return true;
switch (item.getItemId()) {
case R.id.mediapicker_menu_add:
adapter.setForcedMultiSelect(true);
viewModel.onMultiSelectStarted();
return true;
}
return false;
}
@@ -162,7 +158,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
@Override
public void onMediaSelectionOverflow(int maxSelection) {
Toast.makeText(requireContext(), getResources().getQuantityString(R.plurals.MediaPickerItemFragment_cant_share_more_than_n_items, maxSelection, maxSelection), Toast.LENGTH_SHORT).show();
Toast.makeText(requireContext(), getResources().getQuantityString(R.plurals.MediaSendActivity_cant_share_more_than_n_items, maxSelection, maxSelection), Toast.LENGTH_SHORT).show();
}
private void initToolbar(Toolbar toolbar) {

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.mediasend;
import android.Manifest;
import android.arch.lifecycle.ViewModelProviders;
import android.content.Context;
import android.content.Intent;
@@ -19,10 +20,10 @@ import android.widget.Toast;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.camera.Camera1Fragment;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.scribbles.ScribbleFragment;
@@ -58,8 +59,6 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
public static final String EXTRA_MESSAGE = "message";
public static final String EXTRA_TRANSPORT = "transport";
private static final int MAX_PUSH = 32;
private static final int MAX_SMS = 1;
private static final String KEY_ADDRESS = "address";
private static final String KEY_BODY = "body";
@@ -81,6 +80,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
private View countButton;
private TextView countButtonText;
private View cameraButton;
/**
* Get an intent to launch the media send flow starting with the picker.
@@ -134,14 +134,13 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
countButton = findViewById(R.id.mediasend_count_button);
countButtonText = findViewById(R.id.mediasend_count_button_text);
cameraButton = findViewById(R.id.mediasend_camera_button);
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.setMediaConstraints(transport.isSms() ? MediaConstraints.getMmsMediaConstraints(transport.getSimSubscriptionId().or(-1))
: MediaConstraints.getPushMediaConstraints());
viewModel.setTransport(transport);
viewModel.onBodyChanged(getIntent().getStringExtra(KEY_BODY));
List<Media> media = getIntent().getParcelableArrayListExtra(KEY_MEDIA);
@@ -156,7 +155,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
} else if (!Util.isEmpty(media)) {
viewModel.onSelectedMediaChanged(this, media);
Fragment fragment = MediaSendFragment.newInstance(transport, dynamicLanguage.getCurrentLocale());
Fragment fragment = MediaSendFragment.newInstance(recipient, transport, dynamicLanguage.getCurrentLocale());
getSupportFragmentManager().beginTransaction()
.replace(R.id.mediasend_fragment_container, fragment, TAG_SEND)
.commit();
@@ -168,6 +167,18 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
}
initializeCountButtonObserver(transport, dynamicLanguage.getCurrentLocale());
initializeCameraButtonObserver();
initializeErrorObserver();
cameraButton.setOnClickListener(v -> {
int maxSelection = viewModel.getMaxSelection();
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();
} else {
navigateToCamera();
}
});
}
@Override
@@ -189,11 +200,16 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
@Override
public void onFolderSelected(@NonNull MediaFolder folder) {
viewModel.onFolderSelected(folder.getBucketId());
MediaPickerItemFragment fragment = MediaPickerItemFragment.newInstance(folder.getBucketId(), folder.getTitle(), transport.isSms() ? MAX_SMS :MAX_PUSH);
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)
@@ -203,14 +219,14 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
@Override
public void onMediaSelected(@NonNull String bucketId) {
navigateToMediaSend(transport, dynamicLanguage.getCurrentLocale());
navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale());
}
@Override
public void onAddMediaClicked(@NonNull String bucketId) {
// TODO: Get actual folder title somehow
MediaPickerFolderFragment folderFragment = MediaPickerFolderFragment.newInstance(recipient);
MediaPickerItemFragment itemFragment = MediaPickerItemFragment.newInstance(bucketId, "", transport.isSms() ? MAX_SMS : MAX_PUSH);
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)
@@ -302,7 +318,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
Log.i(TAG, "Camera capture stored: " + media.getUri().toString());
viewModel.onImageCaptured(media);
navigateToMediaSend(transport, dynamicLanguage.getCurrentLocale());
navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale());
});
}
@@ -316,26 +332,42 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
if (buttonState == null) return;
countButtonText.setText(String.valueOf(buttonState.getCount()));
countButton.setEnabled(buttonState.getVisibility());
animateCountButtonVisibility(countButton, countButton.getVisibility(), buttonState.getVisibility() ? View.VISIBLE : View.GONE);
countButton.setEnabled(buttonState.isVisible());
animateButtonVisibility(countButton, countButton.getVisibility(), buttonState.isVisible() ? View.VISIBLE : View.GONE);
if (buttonState.getCount() > 0) {
countButton.setOnClickListener(v -> {
Camera1Fragment fragment = (Camera1Fragment) getSupportFragmentManager().findFragmentByTag(TAG_CAMERA);
if (fragment != null) {
fragment.reset();
}
navigateToMediaSend(transport, locale);
});
countButton.setOnClickListener(v -> navigateToMediaSend(recipient, transport, locale));
} else {
countButton.setOnClickListener(null);
}
});
}
private void navigateToMediaSend(@NonNull TransportOption transport, @NonNull Locale locale) {
MediaSendFragment fragment = MediaSendFragment.newInstance(transport, locale);
private void initializeCameraButtonObserver() {
viewModel.getCameraButtonVisibility().observe(this, visible -> {
if (visible == null) return;
animateButtonVisibility(cameraButton, cameraButton.getVisibility(), visible ? View.VISIBLE : View.GONE);
});
}
private void initializeErrorObserver() {
viewModel.getError().observe(this, error -> {
if (error == null) return;
switch (error) {
case ITEM_TOO_LARGE:
Toast.makeText(this, R.string.MediaSendActivity_an_item_was_removed_because_it_exceeded_the_size_limit, Toast.LENGTH_LONG).show();
break;
case TOO_MANY_ITEMS:
int maxSelection = viewModel.getMaxSelection();
Toast.makeText(this, getResources().getQuantityString(R.plurals.MediaSendActivity_cant_share_more_than_n_items, maxSelection, maxSelection), Toast.LENGTH_SHORT).show();
break;
}
});
}
private void navigateToMediaSend(@NonNull Recipient recipient, @NonNull TransportOption transport, @NonNull Locale locale) {
MediaSendFragment fragment = MediaSendFragment.newInstance(recipient, transport, locale);
String backstackTag = null;
if (getSupportFragmentManager().findFragmentByTag(TAG_SEND) != null) {
@@ -350,19 +382,44 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
.commit();
}
private void animateCountButtonVisibility(View countButton, int oldVisibility, int newVisibility) {
private void navigateToCamera() {
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_photo_camera_white_48dp)
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video))
.onAllGranted(() -> {
Camera1Fragment fragment = getOrCreateCameraFragment();
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.slide_from_right, R.anim.slide_to_left, R.anim.slide_from_left, R.anim.slide_to_right)
.replace(R.id.mediasend_fragment_container, fragment, TAG_CAMERA)
.addToBackStack(null)
.commit();
})
.onAnyDenied(() -> Toast.makeText(MediaSendActivity.this, R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show())
.execute();
}
private Camera1Fragment getOrCreateCameraFragment() {
Camera1Fragment fragment = (Camera1Fragment) getSupportFragmentManager().findFragmentByTag(TAG_CAMERA);
return fragment != null ? fragment
: Camera1Fragment.newInstance();
}
private void animateButtonVisibility(View button, int oldVisibility, int newVisibility) {
if (oldVisibility == newVisibility) return;
if (countButton.getAnimation() != null) {
countButton.getAnimation().cancel();
countButton.setVisibility(newVisibility);
if (button.getAnimation() != null) {
button.getAnimation().cancel();
button.setVisibility(newVisibility);
} else if (newVisibility == View.VISIBLE) {
countButton.setVisibility(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());
countButton.startAnimation(animation);
button.startAnimation(animation);
} else {
Animation animation = new ScaleAnimation(1, 0, 1, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
animation.setDuration(150);
@@ -370,12 +427,11 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
animation.setAnimationListener(new SimpleAnimationListener() {
@Override
public void onAnimationEnd(Animation animation) {
countButton.setVisibility(View.GONE);
button.setVisibility(View.GONE);
}
});
countButton.startAnimation(animation);
button.startAnimation(animation);
}
}
}

View File

@@ -42,6 +42,7 @@ 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.widget.ScribbleView;
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
import org.thoughtcrime.securesms.util.MediaUtil;
@@ -51,6 +52,7 @@ import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.views.Stub;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -72,12 +74,12 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
private static final String TAG = MediaSendFragment.class.getSimpleName();
private static final String KEY_ADDRESS = "address";
private static final String KEY_TRANSPORT = "transport";
private static final String KEY_LOCALE = "locale";
private InputAwareLayout hud;
private SendButton sendButton;
private View addButton;
private ComposeText composeText;
private ViewGroup composeContainer;
private EmojiEditText captionText;
@@ -98,8 +100,9 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
private final Rect visibleBounds = new Rect();
public static MediaSendFragment newInstance(@NonNull TransportOption transport, @NonNull Locale locale) {
public static MediaSendFragment newInstance(@NonNull Recipient recipient, @NonNull TransportOption transport, @NonNull Locale locale) {
Bundle args = new Bundle();
args.putParcelable(KEY_ADDRESS, recipient.getAddress());
args.putParcelable(KEY_TRANSPORT, transport);
args.putSerializable(KEY_LOCALE, locale);
@@ -145,7 +148,6 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
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);
addButton = view.findViewById(R.id.mediasend_add_button);
playbackControlsContainer = view.findViewById(R.id.mediasend_playback_controls_container);
charactersLeft = view.findViewById(R.id.mediasend_characters_left);
@@ -205,6 +207,11 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
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);
if (TextSecurePreferences.isSystemEmojiPreferred(getContext())) {
emojiToggle.setVisibility(View.GONE);
@@ -264,12 +271,24 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
@Override
public void onKeyboardShown() {
if (composeText.hasFocus()) {
if (sendButton.getSelectedTransport().isSms()) {
mediaRail.setVisibility(View.GONE);
composeContainer.setVisibility(View.VISIBLE);
captionText.setVisibility(View.GONE);
} else if (captionText.hasFocus()) {
mediaRail.setVisibility(View.GONE);
composeContainer.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);
}
}
}
@@ -277,9 +296,15 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
public void onKeyboardHidden() {
composeContainer.setVisibility(View.VISIBLE);
if (!Util.isEmpty(viewModel.getSelectedMedia().getValue()) && viewModel.getSelectedMedia().getValue().size() > 1) {
if (sendButton.getSelectedTransport().isSms()) {
mediaRail.setVisibility(View.GONE);
captionText.setVisibility(View.GONE);
} else {
mediaRail.setVisibility(View.VISIBLE);
captionText.setVisibility(View.VISIBLE);
if (!Util.isEmpty(viewModel.getSelectedMedia().getValue()) && viewModel.getSelectedMedia().getValue().size() > 1) {
captionText.setVisibility(View.VISIBLE);
}
}
}
@@ -306,7 +331,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
fragmentPagerAdapter.setMedia(media);
mediaRail.setVisibility(media.size() > 1 ? View.VISIBLE : View.GONE);
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);
});
@@ -318,7 +343,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
mediaRailAdapter.setActivePosition(position);
mediaRail.smoothScrollToPosition(position);
if (!fragmentPagerAdapter.getAllMedia().isEmpty()) {
if (fragmentPagerAdapter.getAllMedia().size() > position) {
captionText.setText(fragmentPagerAdapter.getAllMedia().get(position).getCaption().or(""));
}
@@ -337,18 +362,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
viewModel.getBucketId().observe(this, bucketId -> {
if (bucketId == null) return;
if (sendButton.getSelectedTransport().isSms()) {
addButton.setVisibility(View.GONE);
} else {
addButton.setVisibility(View.VISIBLE);
addButton.setOnClickListener(v -> controller.onAddMediaClicked(bucketId));
}
});
viewModel.getError().observe(this, error -> {
if (error == MediaSendViewModel.Error.ITEM_TOO_LARGE) {
Toast.makeText(requireContext(), R.string.MediaSendActivity_an_item_was_removed_because_it_exceeded_the_size_limit, Toast.LENGTH_LONG).show();
}
mediaRailAdapter.setAddButtonListener(() -> controller.onAddMediaClicked(bucketId));
});
}

View File

@@ -12,6 +12,7 @@ import android.text.TextUtils;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil;
@@ -30,6 +31,9 @@ import java.util.Map;
*/
class MediaSendViewModel extends ViewModel {
private static final int MAX_PUSH = 32;
private static final int MAX_SMS = 1;
private final Application application;
private final MediaRepository repository;
private final MutableLiveData<List<Media>> selectedMedia;
@@ -38,6 +42,7 @@ class MediaSendViewModel extends ViewModel {
private final MutableLiveData<String> bucketId;
private final MutableLiveData<List<MediaFolder>> folders;
private final MutableLiveData<CountButtonState> countButtonState;
private final MutableLiveData<Boolean> cameraButtonVisibility;
private final SingleLiveEvent<Error> error;
private final Map<Uri, Object> savedDrawState;
@@ -46,27 +51,37 @@ class MediaSendViewModel extends ViewModel {
private CountButtonState.Visibility countButtonVisibility;
private boolean sentMedia;
private Optional<Media> lastImageCapture;
private int maxSelection;
private MediaSendViewModel(@NonNull Application application, @NonNull MediaRepository repository) {
this.application = application;
this.repository = repository;
this.selectedMedia = new MutableLiveData<>();
this.bucketMedia = new MutableLiveData<>();
this.position = new MutableLiveData<>();
this.bucketId = new MutableLiveData<>();
this.folders = new MutableLiveData<>();
this.countButtonState = new MutableLiveData<>();
this.error = new SingleLiveEvent<>();
this.savedDrawState = new HashMap<>();
this.countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
this.lastImageCapture = Optional.absent();
this.application = application;
this.repository = repository;
this.selectedMedia = new MutableLiveData<>();
this.bucketMedia = new MutableLiveData<>();
this.position = new MutableLiveData<>();
this.bucketId = new MutableLiveData<>();
this.folders = new MutableLiveData<>();
this.countButtonState = new MutableLiveData<>();
this.cameraButtonVisibility = new MutableLiveData<>();
this.error = new SingleLiveEvent<>();
this.savedDrawState = new HashMap<>();
this.countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
this.lastImageCapture = Optional.absent();
this.body = "";
position.setValue(-1);
countButtonState.setValue(new CountButtonState(0, CountButtonState.Visibility.CONDITIONAL));
cameraButtonVisibility.setValue(false);
}
void setMediaConstraints(@NonNull MediaConstraints mediaConstraints) {
this.mediaConstraints = mediaConstraints;
void setTransport(@NonNull TransportOption transport) {
if (transport.isSms()) {
maxSelection = MAX_SMS;
mediaConstraints = MediaConstraints.getMmsMediaConstraints(transport.getSimSubscriptionId().or(-1));
} else {
maxSelection = MAX_PUSH;
mediaConstraints = MediaConstraints.getPushMediaConstraints();
}
}
void onSelectedMediaChanged(@NonNull Context context, @NonNull List<Media> newMedia) {
@@ -75,13 +90,16 @@ class MediaSendViewModel extends ViewModel {
if (filteredMedia.size() != newMedia.size()) {
error.postValue(Error.ITEM_TOO_LARGE);
} else if (filteredMedia.size() > maxSelection) {
filteredMedia = filteredMedia.subList(0, maxSelection);
error.postValue(Error.TOO_MANY_ITEMS);
}
if (filteredMedia.size() > 0) {
String computedId = Stream.of(filteredMedia)
.skip(1)
.reduce(filteredMedia.get(0).getBucketId().orNull(), (id, m) -> {
if (Util.equals(id, m.getBucketId().orNull())) {
.reduce(filteredMedia.get(0).getBucketId().or(Media.ALL_MEDIA_BUCKET_ID), (id, m) -> {
if (Util.equals(id, m.getBucketId().or(Media.ALL_MEDIA_BUCKET_ID))) {
return id;
} else {
return Media.ALL_MEDIA_BUCKET_ID;
@@ -106,6 +124,7 @@ class MediaSendViewModel extends ViewModel {
void onImageEditorStarted() {
countButtonVisibility = CountButtonState.Visibility.FORCED_OFF;
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
cameraButtonVisibility.setValue(false);
}
void onImageEditorEnded() {
@@ -113,6 +132,18 @@ class MediaSendViewModel extends ViewModel {
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
}
void onCameraStarted() {
cameraButtonVisibility.setValue(false);
}
void onItemPickerStarted() {
cameraButtonVisibility.setValue(true);
}
void onFolderPickerStarted() {
cameraButtonVisibility.setValue(true);
}
void onBodyChanged(@NonNull CharSequence body) {
this.body = body;
}
@@ -143,6 +174,11 @@ class MediaSendViewModel extends ViewModel {
selected = new LinkedList<>();
}
if (selected.size() >= maxSelection) {
error.postValue(Error.TOO_MANY_ITEMS);
return;
}
lastImageCapture = Optional.of(media);
selected.add(media);
@@ -208,22 +244,30 @@ class MediaSendViewModel extends ViewModel {
return countButtonState;
}
CharSequence getBody() {
@NonNull LiveData<Boolean> getCameraButtonVisibility() {
return cameraButtonVisibility;
}
@NonNull CharSequence getBody() {
return body;
}
LiveData<Integer> getPosition() {
@NonNull LiveData<Integer> getPosition() {
return position;
}
LiveData<String> getBucketId() {
@NonNull LiveData<String> getBucketId() {
return bucketId;
}
LiveData<Error> getError() {
@NonNull LiveData<Error> getError() {
return error;
}
int getMaxSelection() {
return maxSelection;
}
private @NonNull List<Media> getSelectedMediaOrDefault() {
return selectedMedia.getValue() == null ? Collections.emptyList()
: selectedMedia.getValue();
@@ -252,7 +296,7 @@ class MediaSendViewModel extends ViewModel {
}
enum Error {
ITEM_TOO_LARGE
ITEM_TOO_LARGE, TOO_MANY_ITEMS
}
static class CountButtonState {
@@ -268,7 +312,7 @@ class MediaSendViewModel extends ViewModel {
return count;
}
boolean getVisibility() {
boolean isVisible() {
switch (visibility) {
case FORCED_ON: return true;
case FORCED_OFF: return false;

View File

@@ -1,11 +1,11 @@
package org.thoughtcrime.securesms.camera;
package org.thoughtcrime.securesms.mediasend;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.Stack;
@SuppressWarnings("ConstantConditions")
public class OrderEnforcer<E> {
@@ -22,21 +22,20 @@ public class OrderEnforcer<E> {
if (isCompletedThrough(stage)) {
r.run();
} else {
stages.get(stage).getActions().add(r);
stages.get(stage).addAction(r);
}
}
public synchronized void markCompleted(@NonNull E stage) {
stages.get(stage).setCompleted(true);
stages.get(stage).markCompleted();
for (E s : stages.keySet()) {
StageDetails details = stages.get(s);
if (details.isCompleted()) {
for (Runnable r : details.getActions()) {
r.run();
while (details.hasAction()) {
details.popAction().run();
}
details.getActions().clear();
} else {
break;
}
@@ -45,8 +44,7 @@ public class OrderEnforcer<E> {
public synchronized void reset() {
for (StageDetails details : stages.values()) {
details.setCompleted(false);
details.getActions().clear();
details.reset();
}
}
@@ -62,19 +60,32 @@ public class OrderEnforcer<E> {
}
private static class StageDetails {
private boolean completed = false;
private List<Runnable> actions = new CopyOnWriteArrayList<>();
private boolean completed = false;
private Stack<Runnable> actions = new Stack<>();
@NonNull List<Runnable> getActions() {
return actions;
boolean hasAction() {
return !actions.isEmpty();
}
@Nullable Runnable popAction() {
return actions.pop();
}
void addAction(@NonNull Runnable runnable) {
actions.push(runnable);
}
void reset() {
actions.clear();
completed = false;
}
boolean isCompleted() {
return completed;
}
void setCompleted(boolean completed) {
this.completed = completed;
void markCompleted() {
completed = true;
}
}
}

View File

@@ -41,11 +41,10 @@ import java.util.Set;
public class ScribbleHud extends InputAwareLayout implements ViewTreeObserver.OnGlobalLayoutListener {
private View drawButton;
private View highlightButton;
private View textButton;
private View stickerButton;
private View undoButton;
private View deleteButton;
private View confirmButton;
private View saveButton;
private VerticalSlideColorPicker colorPicker;
private RecyclerView colorPalette;
@@ -111,11 +110,10 @@ public class ScribbleHud extends InputAwareLayout implements ViewTreeObserver.On
setOrientation(VERTICAL);
drawButton = findViewById(R.id.scribble_draw_button);
highlightButton = findViewById(R.id.scribble_highlight_button);
textButton = findViewById(R.id.scribble_text_button);
stickerButton = findViewById(R.id.scribble_sticker_button);
undoButton = findViewById(R.id.scribble_undo_button);
deleteButton = findViewById(R.id.scribble_delete_button);
confirmButton = findViewById(R.id.scribble_confirm_button);
saveButton = findViewById(R.id.scribble_save_button);
colorPicker = findViewById(R.id.scribble_color_picker);
colorPalette = findViewById(R.id.scribble_color_palette);
@@ -163,6 +161,8 @@ public class ScribbleHud extends InputAwareLayout implements ViewTreeObserver.On
setMode(Mode.NONE);
});
confirmButton.setOnClickListener(v -> setMode(Mode.NONE));
sendButton.addOnTransportChangedListener((newTransport, manuallySelected) -> {
presentCharactersRemaining();
composeText.setTransport(newTransport);
@@ -255,84 +255,70 @@ public class ScribbleHud extends InputAwareLayout implements ViewTreeObserver.On
private void presentModeNone() {
drawButton.setVisibility(VISIBLE);
highlightButton.setVisibility(VISIBLE);
textButton.setVisibility(VISIBLE);
stickerButton.setVisibility(VISIBLE);
undoButton.setVisibility(GONE);
deleteButton.setVisibility(GONE);
confirmButton.setVisibility(GONE);
colorPicker.setVisibility(GONE);
colorPalette.setVisibility(GONE);
drawButton.setOnClickListener(v -> setMode(Mode.DRAW));
highlightButton.setOnClickListener(v -> setMode(Mode.HIGHLIGHT));
textButton.setOnClickListener(v -> setMode(Mode.TEXT));
stickerButton.setOnClickListener(v -> setMode(Mode.STICKER));
}
private void presentModeDraw() {
drawButton.setVisibility(VISIBLE);
confirmButton.setVisibility(VISIBLE);
undoButton.setVisibility(VISIBLE);
colorPicker.setVisibility(VISIBLE);
colorPalette.setVisibility(VISIBLE);
highlightButton.setVisibility(GONE);
drawButton.setVisibility(GONE);
textButton.setVisibility(GONE);
stickerButton.setVisibility(GONE);
deleteButton.setVisibility(GONE);
drawButton.setOnClickListener(v -> setMode(Mode.NONE));
colorPicker.setOnColorChangeListener(standardOnColorChangeListener);
colorPicker.setActiveColor(Color.RED);
}
private void presentModeHighlight() {
highlightButton.setVisibility(VISIBLE);
confirmButton.setVisibility(VISIBLE);
undoButton.setVisibility(VISIBLE);
colorPicker.setVisibility(VISIBLE);
colorPalette.setVisibility(VISIBLE);
drawButton.setVisibility(GONE);
textButton.setVisibility(GONE);
stickerButton.setVisibility(GONE);
deleteButton.setVisibility(GONE);
highlightButton.setOnClickListener(v -> setMode(Mode.NONE));
colorPicker.setOnColorChangeListener(highlightOnColorChangeListener);
colorPicker.setActiveColor(Color.YELLOW);
}
private void presentModeText() {
textButton.setVisibility(VISIBLE);
confirmButton.setVisibility(VISIBLE);
deleteButton.setVisibility(VISIBLE);
colorPicker.setVisibility(VISIBLE);
colorPalette.setVisibility(VISIBLE);
textButton.setVisibility(GONE);
drawButton.setVisibility(GONE);
highlightButton.setVisibility(GONE);
stickerButton.setVisibility(GONE);
undoButton.setVisibility(GONE);
textButton.setOnClickListener(v -> setMode(Mode.NONE));
colorPicker.setOnColorChangeListener(standardOnColorChangeListener);
colorPicker.setActiveColor(Color.WHITE);
}
private void presentModeSticker() {
stickerButton.setVisibility(VISIBLE);
deleteButton.setVisibility(VISIBLE);
confirmButton.setVisibility(VISIBLE);
drawButton.setVisibility(GONE);
highlightButton.setVisibility(GONE);
textButton.setVisibility(GONE);
undoButton.setVisibility(GONE);
colorPicker.setVisibility(GONE);
colorPalette.setVisibility(GONE);
stickerButton.setOnClickListener(v -> setMode(Mode.NONE));
}
private void presentCharactersRemaining() {

View File

@@ -136,6 +136,7 @@ public class MotionView extends FrameLayout implements TextWatcher {
this.addView(editText);
this.editText.clearFocus();
this.editText.addTextChangedListener(this);
this.editText.setId(R.id.motion_view_edittext);
// init listeners
this.scaleGestureDetector = new ScaleGestureDetector(context, new ScaleListener());

View File

@@ -43,7 +43,7 @@ import org.thoughtcrime.securesms.R;
public class VerticalSlideColorPicker extends View {
private static final float INDICATOR_TO_BAR_WIDTH_RATIO = 0.8f;
private static final float INDICATOR_TO_BAR_WIDTH_RATIO = 0.5f;
private Paint paint;
private Paint strokePaint;
@@ -131,9 +131,9 @@ public class VerticalSlideColorPicker extends View {
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
path.addCircle(centerX, borderWidth + colorPickerRadius, colorPickerRadius, Path.Direction.CW);
path.addCircle(centerX, borderWidth + colorPickerRadius + indicatorRadius, colorPickerRadius, Path.Direction.CW);
path.addRect(colorPickerBody, Path.Direction.CW);
path.addCircle(centerX, viewHeight - (borderWidth + colorPickerRadius), colorPickerRadius, Path.Direction.CW);
path.addCircle(centerX, viewHeight - (borderWidth + colorPickerRadius + indicatorRadius), colorPickerRadius, Path.Direction.CW);
bitmapCanvas.drawColor(Color.TRANSPARENT);
@@ -178,7 +178,10 @@ public class VerticalSlideColorPicker extends View {
indicatorRadius = (viewWidth / 2) - borderWidth;
colorPickerRadius = (barWidth / 2) - borderWidth;
colorPickerBody = new RectF(centerX - colorPickerRadius, borderWidth + colorPickerRadius, centerX + colorPickerRadius, viewHeight - (borderWidth + colorPickerRadius));
colorPickerBody = new RectF(centerX - colorPickerRadius,
borderWidth + colorPickerRadius + indicatorRadius,
centerX + colorPickerRadius,
viewHeight - (borderWidth + colorPickerRadius + indicatorRadius));
LinearGradient gradient = new LinearGradient(0, colorPickerBody.top, 0, colorPickerBody.bottom, colors, null, Shader.TileMode.CLAMP);
paint.setShader(gradient);