() {
@Override
- public CropAreaRenderer createFromParcel(Parcel in) {
- return new CropAreaRenderer(in);
+ public @NonNull CropAreaRenderer createFromParcel(@NonNull Parcel in) {
+ return new CropAreaRenderer(in.readInt(),
+ in.readByte() == 1);
}
@Override
- public CropAreaRenderer[] newArray(int size) {
+ public @NonNull CropAreaRenderer[] newArray(int size) {
return new CropAreaRenderer[size];
}
};
@Override
- public int describeContents() {
- return 0;
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(color);
+ dest.writeByte((byte) (renderCenterThumbs ? 1 : 0));
}
@Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(color);
+ public int describeContents() {
+ return 0;
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/OvalGuideRenderer.java b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/OvalGuideRenderer.java
new file mode 100644
index 0000000000..643e838824
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/OvalGuideRenderer.java
@@ -0,0 +1,86 @@
+package org.thoughtcrime.securesms.imageeditor.renderers;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.os.Parcel;
+
+import androidx.annotation.ColorRes;
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.imageeditor.Bounds;
+import org.thoughtcrime.securesms.imageeditor.Renderer;
+import org.thoughtcrime.securesms.imageeditor.RendererContext;
+
+/**
+ * Renders an oval inside of the {@link Bounds}.
+ *
+ * Hit tests outside of the bounds.
+ */
+public final class OvalGuideRenderer implements Renderer {
+
+ private final @ColorRes int ovalGuideColor;
+
+ private final Paint paint;
+
+ private final RectF dst = new RectF();
+
+ @Override
+ public void render(@NonNull RendererContext rendererContext) {
+ rendererContext.save();
+
+ Canvas canvas = rendererContext.canvas;
+ Context context = rendererContext.context;
+ int stroke = context.getResources().getDimensionPixelSize(R.dimen.oval_guide_stroke_width);
+ float halfStroke = stroke / 2f;
+
+ this.paint.setStrokeWidth(stroke);
+ paint.setColor(ContextCompat.getColor(context, ovalGuideColor));
+
+ rendererContext.mapRect(dst, Bounds.FULL_BOUNDS);
+ dst.set(dst.left + halfStroke, dst.top + halfStroke, dst.right - halfStroke, dst.bottom - halfStroke);
+
+ rendererContext.canvasMatrix.setToIdentity();
+ canvas.drawOval(dst, paint);
+
+ rendererContext.restore();
+ }
+
+ public OvalGuideRenderer(@ColorRes int color) {
+ this.ovalGuideColor = color;
+
+ this.paint = new Paint();
+ this.paint.setStyle(Paint.Style.STROKE);
+ this.paint.setAntiAlias(true);
+ }
+
+ @Override
+ public boolean hitTest(float x, float y) {
+ return !Bounds.contains(x, y);
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public @NonNull OvalGuideRenderer createFromParcel(@NonNull Parcel in) {
+ return new OvalGuideRenderer(in.readInt());
+ }
+
+ @Override
+ public @NonNull OvalGuideRenderer[] newArray(int size) {
+ return new OvalGuideRenderer[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(ovalGuideColor);
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java
new file mode 100644
index 0000000000..09fa733a69
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java
@@ -0,0 +1,223 @@
+package org.thoughtcrime.securesms.mediasend;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentTransaction;
+import androidx.lifecycle.ViewModelProviders;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.TransportOptions;
+import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
+import org.thoughtcrime.securesms.providers.BlobProvider;
+import org.thoughtcrime.securesms.scribbles.ImageEditorFragment;
+import org.thoughtcrime.securesms.util.MediaUtil;
+import org.whispersystems.libsignal.util.guava.Optional;
+
+import java.io.FileDescriptor;
+import java.util.Collections;
+
+public class AvatarSelectionActivity extends AppCompatActivity implements CameraFragment.Controller, ImageEditorFragment.Controller, MediaPickerFolderFragment.Controller, MediaPickerItemFragment.Controller {
+
+ private static final String IMAGE_CAPTURE = "IMAGE_CAPTURE";
+ private static final String IMAGE_EDITOR = "IMAGE_EDITOR";
+ private static final String ARG_GALLERY = "ARG_GALLERY";
+
+ public static final String EXTRA_MEDIA = "avatar.media";
+
+ private Media currentMedia;
+
+ public static Intent getIntentForCameraCapture(@NonNull Context context) {
+ return new Intent(context, AvatarSelectionActivity.class);
+ }
+
+ public static Intent getIntentForGallery(@NonNull Context context) {
+ Intent intent = getIntentForCameraCapture(context);
+
+ intent.putExtra(ARG_GALLERY, true);
+
+ return intent;
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.avatar_selection_activity);
+
+ MediaSendViewModel viewModel = ViewModelProviders.of(this, new MediaSendViewModel.Factory(getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
+ viewModel.setTransport(TransportOptions.getPushTransportOption(this));
+
+ if (isGalleryFirst()) {
+ onGalleryClicked();
+ } else {
+ onCameraSelected();
+ }
+ }
+
+ @Override
+ public void onCameraError() {
+ Toast.makeText(this, R.string.error, Toast.LENGTH_SHORT).show();
+ finish();
+ }
+
+ @Override
+ public void onImageCaptured(@NonNull byte[] data, int width, int height) {
+ Uri blobUri = BlobProvider.getInstance()
+ .forData(data)
+ .withMimeType(MediaUtil.IMAGE_JPEG)
+ .createForSingleSessionInMemory();
+
+ onMediaSelected(new Media(blobUri,
+ MediaUtil.IMAGE_JPEG,
+ System.currentTimeMillis(),
+ width,
+ height,
+ data.length,
+ 0,
+ Optional.of(Media.ALL_MEDIA_BUCKET_ID),
+ Optional.absent(),
+ Optional.absent()));
+ }
+
+ @Override
+ public void onVideoCaptured(@NonNull FileDescriptor fd) {
+ throw new UnsupportedOperationException("Cannot set profile as video");
+ }
+
+ @Override
+ public void onVideoCaptureError() {
+ throw new AssertionError("This should never happen");
+ }
+
+ @Override
+ public void onGalleryClicked() {
+ if (isGalleryFirst() && popToRoot()) {
+ return;
+ }
+
+ MediaPickerFolderFragment fragment = MediaPickerFolderFragment.newInstance(this, null);
+ FragmentTransaction transaction = getSupportFragmentManager().beginTransaction()
+ .replace(R.id.fragment_container, fragment);
+
+ if (isCameraFirst()) {
+ transaction.addToBackStack(null);
+ }
+
+ transaction.commit();
+ }
+
+ @Override
+ public int getDisplayRotation() {
+ return getWindowManager().getDefaultDisplay().getRotation();
+ }
+
+ @Override
+ public void onCameraCountButtonClicked() {
+ throw new UnsupportedOperationException("Cannot select more than one photo");
+ }
+
+ @Override
+ public void onTouchEventsNeeded(boolean needed) {
+ }
+
+ @Override
+ public void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard) {
+ }
+
+ @Override
+ public void onFolderSelected(@NonNull MediaFolder folder) {
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.fragment_container, MediaPickerItemFragment.newInstance(folder.getBucketId(), folder.getTitle(), 1, false))
+ .addToBackStack(null)
+ .commit();
+ }
+
+ @Override
+ public void onMediaSelected(@NonNull Media media) {
+ currentMedia = media;
+
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.fragment_container, ImageEditorFragment.newInstanceForAvatar(media.getUri()), IMAGE_EDITOR)
+ .addToBackStack(IMAGE_EDITOR)
+ .commit();
+ }
+
+ @Override
+ public void onCameraSelected() {
+ if (isCameraFirst() && popToRoot()) {
+ return;
+ }
+
+ Fragment fragment = CameraFragment.newInstanceForAvatarCapture();
+ FragmentTransaction transaction = getSupportFragmentManager().beginTransaction()
+ .replace(R.id.fragment_container, fragment, IMAGE_CAPTURE);
+
+ if (isGalleryFirst()) {
+ transaction.addToBackStack(null);
+ }
+
+ transaction.commit();
+ }
+
+ @Override
+ public void onDoneEditing() {
+ handleSave();
+ }
+
+ public boolean popToRoot() {
+ final int backStackCount = getSupportFragmentManager().getBackStackEntryCount();
+ if (backStackCount == 0) {
+ return false;
+ }
+
+ for (int i = 0; i < backStackCount; i++) {
+ getSupportFragmentManager().popBackStack();
+ }
+
+ return true;
+ }
+
+ private boolean isGalleryFirst() {
+ return getIntent().getBooleanExtra(ARG_GALLERY, false);
+ }
+
+ private boolean isCameraFirst() {
+ return !isGalleryFirst();
+ }
+
+ private void handleSave() {
+ ImageEditorFragment fragment = (ImageEditorFragment) getSupportFragmentManager().findFragmentByTag(IMAGE_EDITOR);
+ if (fragment == null) {
+ throw new AssertionError();
+ }
+
+ ImageEditorFragment.Data data = (ImageEditorFragment.Data) fragment.saveState();
+ if (data == null) {
+ throw new AssertionError();
+ }
+
+ EditorModel model = data.readModel();
+ if (model == null) {
+ throw new AssertionError();
+ }
+
+ MediaRepository.transformMedia(this,
+ Collections.singletonList(currentMedia),
+ Collections.singletonMap(currentMedia, new ImageEditorModelRenderMediaTransform(model)),
+ output -> {
+ Media transformed = output.get(currentMedia);
+
+ Intent result = new Intent();
+ result.putExtra(EXTRA_MEDIA, transformed);
+ setResult(RESULT_OK, result);
+ finish();
+ });
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionBottomSheetDialogFragment.java
new file mode 100644
index 0000000000..7e3fedb6e5
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionBottomSheetDialogFragment.java
@@ -0,0 +1,199 @@
+package org.thoughtcrime.securesms.mediasend;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.AttrRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.appcompat.widget.AppCompatTextView;
+import androidx.core.util.Consumer;
+import androidx.fragment.app.DialogFragment;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.annimon.stream.Stream;
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.util.ThemeUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AvatarSelectionBottomSheetDialogFragment extends BottomSheetDialogFragment {
+
+ private static final String ARG_OPTIONS = "options";
+ private static final String ARG_REQUEST_CODE = "request_code";
+
+ public static DialogFragment create(boolean includeClear, boolean includeCamera, short resultCode) {
+ DialogFragment fragment = new AvatarSelectionBottomSheetDialogFragment();
+ List selectionOptions = new ArrayList<>(3);
+ Bundle args = new Bundle();
+
+ if (includeCamera) {
+ selectionOptions.add(SelectionOption.CAPTURE);
+ }
+
+ selectionOptions.add(SelectionOption.GALLERY);
+
+ if (includeClear) {
+ selectionOptions.add(SelectionOption.DELETE);
+ }
+
+ String[] options = Stream.of(selectionOptions)
+ .map(SelectionOption::getCode)
+ .toArray(String[]::new);
+
+ args.putStringArray(ARG_OPTIONS, options);
+ args.putShort(ARG_REQUEST_CODE, resultCode);
+ fragment.setArguments(args);
+
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ setStyle(DialogFragment.STYLE_NORMAL,
+ ThemeUtil.isDarkTheme(requireContext()) ? R.style.Theme_Design_BottomSheetDialog_Fixed
+ : R.style.Theme_Design_Light_BottomSheetDialog_Fixed);
+
+ super.onCreate(savedInstanceState);
+
+ if (getOptionsCount() == 1) {
+ launchOptionAndDismiss(getOptionsFromArguments().get(0));
+ }
+ }
+
+ @Override
+ public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.avatar_selection_bottom_sheet_dialog_fragment, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ RecyclerView recyclerView = view.findViewById(R.id.avatar_selection_bottom_sheet_dialog_fragment_recycler);
+ recyclerView.setAdapter(new SelectionOptionAdapter(getOptionsFromArguments(), this::launchOptionAndDismiss));
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ private int getOptionsCount() {
+ return requireArguments().getStringArray(ARG_OPTIONS).length;
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ private List getOptionsFromArguments() {
+ String[] optionCodes = requireArguments().getStringArray(ARG_OPTIONS);
+
+ return Stream.of(optionCodes).map(SelectionOption::fromCode).toList();
+ }
+
+ private void launchOptionAndDismiss(@NonNull SelectionOption option) {
+ Intent intent = createIntent(requireContext(), option);
+
+ int requestCode = requireArguments().getShort(ARG_REQUEST_CODE);
+ if (getParentFragment() != null) {
+ requireParentFragment().startActivityForResult(intent, requestCode);
+ } else {
+ requireActivity().startActivityForResult(intent, requestCode);
+ }
+
+ dismiss();
+ }
+
+ private static Intent createIntent(@NonNull Context context, @NonNull SelectionOption selectionOption) {
+ switch (selectionOption) {
+ case CAPTURE:
+ return AvatarSelectionActivity.getIntentForCameraCapture(context);
+ case GALLERY:
+ return AvatarSelectionActivity.getIntentForGallery(context);
+ case DELETE:
+ return new Intent("org.thoughtcrime.securesms.action.CLEAR_PROFILE_PHOTO");
+ default:
+ throw new IllegalStateException("Unknown option: " + selectionOption);
+ }
+ }
+
+ private enum SelectionOption {
+ CAPTURE("capture", R.string.AvatarSelectionBottomSheetDialogFragment__take_photo, R.attr.avatar_selection_take_photo),
+ GALLERY("gallery", R.string.AvatarSelectionBottomSheetDialogFragment__choose_from_gallery, R.attr.avatar_selection_pick_photo),
+ DELETE("delete", R.string.AvatarSelectionBottomSheetDialogFragment__remove_photo, R.attr.avatar_selection_remove_photo);
+
+ private final String code;
+ private final @StringRes int label;
+ private final @AttrRes int icon;
+
+ SelectionOption(@NonNull String code, @StringRes int label, @AttrRes int icon) {
+ this.code = code;
+ this.label = label;
+ this.icon = icon;
+ }
+
+ public @NonNull String getCode() {
+ return code;
+ }
+
+ static SelectionOption fromCode(@NonNull String code) {
+ for (SelectionOption option : values()) {
+ if (option.code.equals(code)) {
+ return option;
+ }
+ }
+
+ throw new IllegalStateException("Unknown option: " + code);
+ }
+ }
+
+ private static class SelectionOptionViewHolder extends RecyclerView.ViewHolder {
+
+ private final AppCompatTextView optionView;
+
+ SelectionOptionViewHolder(@NonNull View itemView, @NonNull Consumer onClick) {
+ super(itemView);
+ itemView.setOnClickListener(v -> {
+ if (getAdapterPosition() != RecyclerView.NO_POSITION) {
+ onClick.accept(getAdapterPosition());
+ }
+ });
+
+ optionView = (AppCompatTextView) itemView;
+ }
+
+ void bind(@NonNull SelectionOption selectionOption) {
+ optionView.setCompoundDrawablesWithIntrinsicBounds(ThemeUtil.getThemedDrawable(optionView.getContext(), selectionOption.icon), null, null, null);
+ optionView.setText(selectionOption.label);
+ }
+ }
+
+ private static class SelectionOptionAdapter extends RecyclerView.Adapter {
+
+ private final List options;
+ private final Consumer onOptionClicked;
+
+ private SelectionOptionAdapter(@NonNull List options, @NonNull Consumer onOptionClicked) {
+ this.options = options;
+ this.onOptionClicked = onOptionClicked;
+ }
+
+ @NonNull
+ @Override
+ public SelectionOptionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.avatar_selection_bottom_sheet_dialog_fragment_option, parent, false);
+ return new SelectionOptionViewHolder(view, (position) -> onOptionClicked.accept(options.get(position)));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull SelectionOptionViewHolder holder, int position) {
+ holder.bind(options.get(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return options.size();
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraFragment.java
index afd384ac7e..439f061f3f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraFragment.java
@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.mediasend;
import android.annotation.SuppressLint;
-import android.os.Build;
import androidx.annotation.NonNull;
import androidx.camera.core.CameraX;
@@ -10,8 +9,6 @@ import androidx.fragment.app.Fragment;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
import java.io.FileDescriptor;
-import java.util.HashSet;
-import java.util.Set;
public interface CameraFragment {
@@ -24,6 +21,15 @@ public interface CameraFragment {
}
}
+ @SuppressLint("RestrictedApi")
+ static Fragment newInstanceForAvatarCapture() {
+ if (CameraXUtil.isSupported() && CameraX.isInitialized()) {
+ return CameraXFragment.newInstanceForAvatarCapture();
+ } else {
+ return Camera1Fragment.newInstance();
+ }
+ }
+
interface Controller {
void onCameraError();
void onImageCaptured(@NonNull byte[] data, int width, int height);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java
index 68f961efeb..8ea39dad91 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java
@@ -25,7 +25,6 @@ import androidx.annotation.RequiresApi;
import androidx.camera.core.CameraX;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageProxy;
-import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
@@ -45,7 +44,6 @@ import org.thoughtcrime.securesms.util.MemoryFileDescriptor;
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.SimpleTask;
import org.thoughtcrime.securesms.video.VideoUtil;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -60,7 +58,8 @@ import java.io.IOException;
@RequiresApi(21)
public class CameraXFragment extends Fragment implements CameraFragment {
- private static final String TAG = Log.tag(CameraXFragment.class);
+ private static final String TAG = Log.tag(CameraXFragment.class);
+ private static final String IS_VIDEO_ENABLED = "is_video_enabled";
private CameraXView camera;
private ViewGroup controlsContainer;
@@ -69,8 +68,22 @@ public class CameraXFragment extends Fragment implements CameraFragment {
private View selfieFlash;
private MemoryFileDescriptor videoFileDescriptor;
+ public static CameraXFragment newInstanceForAvatarCapture() {
+ CameraXFragment fragment = new CameraXFragment();
+ Bundle args = new Bundle();
+
+ args.putBoolean(IS_VIDEO_ENABLED, false);
+ fragment.setArguments(args);
+
+ return fragment;
+ }
+
public static CameraXFragment newInstance() {
- return new CameraXFragment();
+ CameraXFragment fragment = new CameraXFragment();
+
+ fragment.setArguments(new Bundle());
+
+ return fragment;
}
@Override
@@ -282,9 +295,10 @@ public class CameraXFragment extends Fragment implements CameraFragment {
}
private boolean isVideoRecordingSupported(@NonNull Context context) {
- return Build.VERSION.SDK_INT >= 26 &&
- MediaConstraints.isVideoTranscodeAvailable() &&
- CameraXUtil.isMixedModeSupported(context) &&
+ return Build.VERSION.SDK_INT >= 26 &&
+ requireArguments().getBoolean(IS_VIDEO_ENABLED, true) &&
+ MediaConstraints.isVideoTranscodeAvailable() &&
+ CameraXUtil.isMixedModeSupported(context) &&
VideoUtil.getMaxVideoDurationInSeconds(context, viewModel.getMediaConstraints()) > 0;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java
index 09a026643a..59e86a2cc7 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java
@@ -1,25 +1,25 @@
package org.thoughtcrime.securesms.mediasend;
-import androidx.lifecycle.ViewModelProviders;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Point;
import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.appcompat.app.AppCompatActivity;
-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;
import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.util.Util;
@@ -32,9 +32,10 @@ import java.util.List;
*/
public class MediaPickerItemFragment extends Fragment implements MediaPickerItemAdapter.EventListener {
- private static final String KEY_BUCKET_ID = "bucket_id";
- private static final String KEY_FOLDER_TITLE = "folder_title";
- private static final String KEY_MAX_SELECTION = "max_selection";
+ private static final String KEY_BUCKET_ID = "bucket_id";
+ private static final String KEY_FOLDER_TITLE = "folder_title";
+ private static final String KEY_MAX_SELECTION = "max_selection";
+ private static final String KEY_FORCE_MULTI_SELECT = "force_multi_select";
private String bucketId;
private String folderTitle;
@@ -45,10 +46,15 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
private GridLayoutManager layoutManager;
public static MediaPickerItemFragment newInstance(@NonNull String bucketId, @NonNull String folderTitle, int maxSelection) {
+ return newInstance(bucketId, folderTitle, maxSelection, true);
+ }
+
+ public static MediaPickerItemFragment newInstance(@NonNull String bucketId, @NonNull String folderTitle, int maxSelection, boolean forceMultiSelect) {
Bundle args = new Bundle();
args.putString(KEY_BUCKET_ID, bucketId);
args.putString(KEY_FOLDER_TITLE, folderTitle);
args.putInt(KEY_MAX_SELECTION, maxSelection);
+ args.putBoolean(KEY_FORCE_MULTI_SELECT, forceMultiSelect);
MediaPickerItemFragment fragment = new MediaPickerItemFragment();
fragment.setArguments(args);
@@ -110,8 +116,10 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
public void onResume() {
super.onResume();
viewModel.onItemPickerStarted();
- adapter.setForcedMultiSelect(true);
- viewModel.onMultiSelectStarted();
+ if (requireArguments().getBoolean(KEY_FORCE_MULTI_SELECT)) {
+ adapter.setForcedMultiSelect(true);
+ viewModel.onMultiSelectStarted();
+ }
}
@Override
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java
index 29dd638270..d04e93588c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java
@@ -470,6 +470,10 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
}
}
+ @Override
+ public void onDoneEditing() {
+ }
+
@Override
public void onGlobalLayout() {
hud.getRootView().getWindowVisibleDisplayFrame(visibleBounds);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java
index 8edec4a81d..a51e15a248 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java
@@ -2,10 +2,8 @@ package org.thoughtcrime.securesms.profiles.edit;
import android.Manifest;
import android.animation.Animator;
-import android.app.Activity;
import android.content.Context;
import android.content.Intent;
-import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
@@ -31,28 +29,31 @@ import androidx.navigation.Navigation;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.dd.CircularProgressButton;
+import com.google.android.gms.common.util.IOUtils;
import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.avatar.AvatarSelection;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.PinUtil;
import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity;
+import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFragment;
+import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.megaphone.Megaphones;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.permissions.Permissions;
-import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints;
import org.thoughtcrime.securesms.profiles.ProfileName;
-import org.thoughtcrime.securesms.util.BitmapDecodingException;
-import org.thoughtcrime.securesms.util.BitmapUtil;
+import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
import org.whispersystems.libsignal.util.guava.Optional;
-import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import static android.app.Activity.RESULT_OK;
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.DISPLAY_USERNAME;
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.EXCLUDE_SYSTEM;
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.NEXT_BUTTON_TEXT;
@@ -61,8 +62,9 @@ import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.SHOW_
public class EditProfileFragment extends Fragment {
- private static final String TAG = Log.tag(EditProfileFragment.class);
- private static final String AVATAR_STATE = "avatar";
+ private static final String TAG = Log.tag(EditProfileFragment.class);
+ private static final String AVATAR_STATE = "avatar";
+ private static final short REQUEST_CODE_SELECT_AVATAR = 31726;
private Toolbar toolbar;
private View title;
@@ -77,7 +79,6 @@ public class EditProfileFragment extends Fragment {
private TextView username;
private Intent nextIntent;
- private File captureFile;
private EditProfileViewModel viewModel;
@@ -151,52 +152,38 @@ public class EditProfileFragment extends Fragment {
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
- switch (requestCode) {
- case AvatarSelection.REQUEST_CODE_AVATAR:
- if (resultCode == Activity.RESULT_OK) {
- Uri outputFile = Uri.fromFile(new File(requireActivity().getCacheDir(), "cropped"));
- Uri inputFile = (data != null ? data.getData() : null);
+ if (requestCode == REQUEST_CODE_SELECT_AVATAR && resultCode == RESULT_OK) {
- if (inputFile == null && captureFile != null) {
- inputFile = Uri.fromFile(captureFile);
- }
+ if (data != null && data.getBooleanExtra("delete", false)) {
+ viewModel.setAvatar(null);
+ avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_camera_solid_white_24).asDrawable(requireActivity(), getResources().getColor(R.color.grey_400)));
+ return;
+ }
- if (data != null && data.getBooleanExtra("delete", false)) {
- viewModel.setAvatar(null);
- avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_camera_solid_white_24).asDrawable(requireActivity(), getResources().getColor(R.color.grey_400)));
- } else {
- AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar);
- }
+ SimpleTask.run(() -> {
+ try {
+ Media result = data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA);
+ InputStream stream = BlobProvider.getInstance().getStream(requireContext(), result.getUri());
+
+ return IOUtils.readInputStreamFully(stream);
+ } catch (IOException ioException) {
+ Log.w(TAG, ioException);
+ return null;
}
-
- break;
- case AvatarSelection.REQUEST_CODE_CROP_IMAGE:
- if (resultCode == Activity.RESULT_OK) {
- SimpleTask.run(() -> {
- try {
- BitmapUtil.ScaleResult result = BitmapUtil.createScaledBytes(requireActivity(), AvatarSelection.getResultUri(data), new ProfileMediaConstraints());
- return result.getBitmap();
- } catch (BitmapDecodingException e) {
- Log.w(TAG, e);
- return null;
- }
- },
- (avatarBytes) -> {
- if (avatarBytes != null) {
- viewModel.setAvatar(avatarBytes);
- GlideApp.with(EditProfileFragment.this)
- .load(avatarBytes)
- .skipMemoryCache(true)
- .diskCacheStrategy(DiskCacheStrategy.NONE)
- .circleCrop()
- .into(avatar);
- } else {
- Toast.makeText(requireActivity(), R.string.CreateProfileActivity_error_setting_profile_photo, Toast.LENGTH_LONG).show();
- }
- }
- );
+ },
+ (avatarBytes) -> {
+ if (avatarBytes != null) {
+ viewModel.setAvatar(avatarBytes);
+ GlideApp.with(EditProfileFragment.this)
+ .load(avatarBytes)
+ .skipMemoryCache(true)
+ .diskCacheStrategy(DiskCacheStrategy.NONE)
+ .circleCrop()
+ .into(avatar);
+ } else {
+ Toast.makeText(requireActivity(), R.string.CreateProfileActivity_error_setting_profile_photo, Toast.LENGTH_LONG).show();
}
- break;
+ });
}
}
@@ -314,18 +301,12 @@ public class EditProfileFragment extends Fragment {
}
private void startAvatarSelection() {
- captureFile = AvatarSelection.startAvatarSelection(this, viewModel.hasAvatar(), true);
+ AvatarSelectionBottomSheetDialogFragment.create(viewModel.hasAvatar(), true, REQUEST_CODE_SELECT_AVATAR).show(getChildFragmentManager(), null);
}
private void handleUpload() {
viewModel.submitProfile(uploadResult -> {
if (uploadResult == EditProfileRepository.UploadResult.SUCCESS) {
- if (captureFile != null) {
- if (!captureFile.delete()) {
- Log.w(TAG, "Failed to delete capture file " + captureFile);
- }
- }
-
if (!PinUtil.shouldShowPinCreationDuringRegistration(requireContext())) {
SignalStore.registrationValues().setRegistrationComplete();
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java
index 0a98afd522..92cc62f44a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java
@@ -47,7 +47,8 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
private static final String TAG = Log.tag(ImageEditorFragment.class);
- private static final String KEY_IMAGE_URI = "image_uri";
+ private static final String KEY_IMAGE_URI = "image_uri";
+ private static final String KEY_IS_AVATAR_MODE = "avatar_mode";
private static final int SELECT_OLD_STICKER_REQUEST_CODE = 123;
private static final int SELECT_NEW_STICKER_REQUEST_CODE = 124;
@@ -89,6 +90,12 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
private ImageEditorHud imageEditorHud;
private ImageEditorView imageEditorView;
+ public static ImageEditorFragment newInstanceForAvatar(@NonNull Uri imageUri) {
+ ImageEditorFragment fragment = newInstance(imageUri);
+ fragment.requireArguments().putBoolean(KEY_IS_AVATAR_MODE, true);
+ return fragment;
+ }
+
public static ImageEditorFragment newInstance(@NonNull Uri imageUri) {
Bundle args = new Bundle();
args.putParcelable(KEY_IMAGE_URI, imageUri);
@@ -133,6 +140,8 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
+ boolean isAvatarMode = requireArguments().getBoolean(KEY_IS_AVATAR_MODE, false);
+
imageEditorHud = view.findViewById(R.id.scribble_hud);
imageEditorView = view.findViewById(R.id.image_editor_view);
@@ -150,12 +159,17 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
}
if (editorModel == null) {
- editorModel = new EditorModel();
+ editorModel = isAvatarMode ? EditorModel.createForCircleEditing() : EditorModel.create();
EditorElement image = new EditorElement(new UriGlideRenderer(imageUri, true, imageMaxWidth, imageMaxHeight));
image.getFlags().setSelectable(false).persist();
editorModel.addElement(image);
}
+ if (isAvatarMode) {
+ imageEditorHud.setUpForAvatarEditing();
+ imageEditorHud.enterMode(ImageEditorHud.Mode.CROP);
+ }
+
imageEditorView.setModel(editorModel);
refreshUniqueColors();
@@ -381,6 +395,11 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
controller.onRequestFullScreen(fullScreen, hideKeyboard);
}
+ @Override
+ public void onDone() {
+ controller.onDoneEditing();
+ }
+
private void refreshUniqueColors() {
imageEditorHud.setColorPalette(imageEditorView.getModel().getUniqueColorsIgnoringAlpha());
}
@@ -439,5 +458,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
void onTouchEventsNeeded(boolean needed);
void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard);
+
+ void onDoneEditing();
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorHud.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorHud.java
index 31340eead4..a928d895cc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorHud.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorHud.java
@@ -7,7 +7,6 @@ import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
-import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
@@ -41,6 +40,7 @@ public final class ImageEditorHud extends LinearLayout {
private View saveButton;
private View deleteButton;
private View confirmButton;
+ private View doneButton;
private VerticalSlideColorPicker colorPicker;
private RecyclerView colorPalette;
@@ -88,6 +88,7 @@ public final class ImageEditorHud extends LinearLayout {
deleteButton = findViewById(R.id.scribble_delete_button);
confirmButton = findViewById(R.id.scribble_confirm_button);
colorPicker = findViewById(R.id.scribble_color_picker);
+ doneButton = findViewById(R.id.scribble_done_button);
cropAspectLock.setOnClickListener(v -> {
eventListener.onCropAspectLock(!eventListener.isCropAspectLocked());
@@ -123,6 +124,7 @@ public final class ImageEditorHud extends LinearLayout {
}
allViews.add(stickerButton);
+ allViews.add(doneButton);
}
private void setVisibleViewsWhenInMode(Mode mode, View... views) {
@@ -154,6 +156,20 @@ public final class ImageEditorHud extends LinearLayout {
textButton.setOnClickListener(v -> setMode(Mode.TEXT));
stickerButton.setOnClickListener(v -> setMode(Mode.INSERT_STICKER));
saveButton.setOnClickListener(v -> eventListener.onSave());
+ doneButton.setOnClickListener(v -> eventListener.onDone());
+ }
+
+ public void setUpForAvatarEditing() {
+ visibilityModeMap.get(Mode.NONE).add(doneButton);
+ visibilityModeMap.get(Mode.NONE).remove(saveButton);
+ visibilityModeMap.get(Mode.CROP).remove(cropAspectLock);
+
+ if (currentMode == Mode.NONE) {
+ doneButton.setVisibility(View.VISIBLE);
+ saveButton.setVisibility(View.GONE);
+ } else if (currentMode == Mode.CROP) {
+ cropAspectLock.setVisibility(View.GONE);
+ }
}
public void setColorPalette(@NonNull Set colors) {
@@ -266,6 +282,7 @@ public final class ImageEditorHud extends LinearLayout {
void onCropAspectLock(boolean locked);
boolean isCropAspectLocked();
void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard);
+ void onDone();
}
private static final EventListener NULL_EVENT_LISTENER = new EventListener() {
@@ -310,5 +327,9 @@ public final class ImageEditorHud extends LinearLayout {
@Override
public void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard) {
}
+
+ @Override
+ public void onDone() {
+ }
};
}
diff --git a/app/src/main/res/layout/avatar_selection_activity.xml b/app/src/main/res/layout/avatar_selection_activity.xml
new file mode 100644
index 0000000000..1d249e3e93
--- /dev/null
+++ b/app/src/main/res/layout/avatar_selection_activity.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/app/src/main/res/layout/avatar_selection_bottom_sheet_dialog_fragment.xml b/app/src/main/res/layout/avatar_selection_bottom_sheet_dialog_fragment.xml
new file mode 100644
index 0000000000..abf1a1e261
--- /dev/null
+++ b/app/src/main/res/layout/avatar_selection_bottom_sheet_dialog_fragment.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/avatar_selection_bottom_sheet_dialog_fragment_option.xml b/app/src/main/res/layout/avatar_selection_bottom_sheet_dialog_fragment_option.xml
new file mode 100644
index 0000000000..06933bed4e
--- /dev/null
+++ b/app/src/main/res/layout/avatar_selection_bottom_sheet_dialog_fragment_option.xml
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/image_editor_hud.xml b/app/src/main/res/layout/image_editor_hud.xml
index 5c4f442be2..7428388af1 100644
--- a/app/src/main/res/layout/image_editor_hud.xml
+++ b/app/src/main/res/layout/image_editor_hud.xml
@@ -1,13 +1,12 @@
-
+ tools:background="@color/core_grey_60"
+ tools:parentTag="android.widget.LinearLayout">
@@ -57,7 +56,7 @@
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="8dp"
android:src="@drawable/ic_text_32" />
-
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/crop_area_renderer.xml b/app/src/main/res/values/crop_area_renderer.xml
index 953c9c04ca..b21eda8fee 100644
--- a/app/src/main/res/values/crop_area_renderer.xml
+++ b/app/src/main/res/values/crop_area_renderer.xml
@@ -3,8 +3,10 @@
32dp
2dp
+ 1dp
#ffffffff
#7f000000
+ #66FFFFFF
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f794de8034..cd78befb46 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -355,6 +355,12 @@
Using default: %s
None
+
+ Choose photo
+ Take photo
+ Choose from gallery
+ Remove photo
+
Now
%dm
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 42e3cc664f..2382a4da6e 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -368,6 +368,10 @@
- @color/core_grey_90
- @color/core_grey_60
+ - @drawable/ic_camera_outline_24
+ - @drawable/ic_photo_outline_24
+ - @drawable/ic_trash_outline_24
+
- @drawable/ic_audio_light
- @drawable/ic_video_light
@@ -631,6 +635,10 @@
- @color/core_grey_05
- @color/core_grey_25
+ - @drawable/ic_camera_solid_24
+ - @drawable/ic_photo_solid_24
+ - @drawable/ic_trash_solid_24
+
- @drawable/ic_audio_dark
- @drawable/ic_video_dark
diff --git a/app/witness-verifications.gradle b/app/witness-verifications.gradle
index bd034da0e2..a16e99cfb2 100644
--- a/app/witness-verifications.gradle
+++ b/app/witness-verifications.gradle
@@ -321,9 +321,6 @@ dependencyVerification {
['com.takisoft.fix:colorpicker:0.9.1',
'f5d0dbabe406a1800498ca9c1faf34db36e021d8488bf10360f29961fe3ab0d1'],
- ['com.theartofdev.edmodo:android-image-cropper:2.8.0',
- '5516ea87672e478b3d0ed5c274a7df27d22c02e66f899388f9b8bee93669e176'],
-
['com.tomergoldst.android:tooltips:1.0.6',
'4c56697dd1ad64b8066535c61f961a6d901e7ae5d97ae27084ba40ad620349b6'],