diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d82267a0a7..f55961a792 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -552,6 +552,15 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:launchMode="singleTask" />
+
+
+
+
diff --git a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorView.java b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorView.java
index 18784a167d..7a4a0f8c71 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorView.java
@@ -61,6 +61,9 @@ public final class ImageEditorView extends FrameLayout {
@Nullable
private DrawingChangedListener drawingChangedListener;
+ @Nullable
+ private SizeChangedListener sizeChangedListener;
+
@Nullable
private UndoRedoStackListener undoRedoStackListener;
@@ -93,7 +96,7 @@ public final class ImageEditorView extends FrameLayout {
private void init() {
setWillNotDraw(false);
- setModel(new EditorModel());
+ setModel(EditorModel.create());
editText = createAHiddenTextEntryField();
@@ -170,6 +173,9 @@ public final class ImageEditorView extends FrameLayout {
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
updateViewMatrix();
+ if (sizeChangedListener != null) {
+ sizeChangedListener.onSizeChanged(w, h);
+ }
}
private void updateViewMatrix() {
@@ -393,6 +399,10 @@ public final class ImageEditorView extends FrameLayout {
this.drawingChangedListener = drawingChangedListener;
}
+ public void setSizeChangedListener(@Nullable SizeChangedListener sizeChangedListener) {
+ this.sizeChangedListener = sizeChangedListener;
+ }
+
public void setUndoRedoStackListener(@Nullable UndoRedoStackListener undoRedoStackListener) {
this.undoRedoStackListener = undoRedoStackListener;
}
@@ -463,6 +473,10 @@ public final class ImageEditorView extends FrameLayout {
void onDrawingChanged();
}
+ public interface SizeChangedListener {
+ void onSizeChanged(int newWidth, int newHeight);
+ }
+
public interface TapListener {
void onEntityDown(@Nullable EditorElement editorElement);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElementHierarchy.java b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElementHierarchy.java
index cab52e3f48..2c7cb59359 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElementHierarchy.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElementHierarchy.java
@@ -45,11 +45,15 @@ import org.thoughtcrime.securesms.imageeditor.renderers.OvalGuideRenderer;
final class EditorElementHierarchy {
static @NonNull EditorElementHierarchy create() {
- return new EditorElementHierarchy(createRoot(false));
+ return new EditorElementHierarchy(createRoot(CropStyle.RECTANGLE));
}
static @NonNull EditorElementHierarchy createForCircleEditing() {
- return new EditorElementHierarchy(createRoot(true));
+ return new EditorElementHierarchy(createRoot(CropStyle.CIRCLE));
+ }
+
+ static @NonNull EditorElementHierarchy createForPinchAndPanCropping() {
+ return new EditorElementHierarchy(createRoot(CropStyle.PINCH_AND_PAN));
}
static @NonNull EditorElementHierarchy create(@NonNull EditorElement root) {
@@ -78,7 +82,24 @@ final class EditorElementHierarchy {
this.thumbs = this.cropEditorElement.getChild(1);
}
- private static @NonNull EditorElement createRoot(boolean circleEdit) {
+ private enum CropStyle {
+ /**
+ * A rectangular overlay with 8 thumbs, corners and edges.
+ */
+ RECTANGLE,
+
+ /**
+ * Cropping with a circular template overlay with Corner thumbs only.
+ */
+ CIRCLE,
+
+ /**
+ * No overlay and no thumbs. Cropping achieved through pinching and panning.
+ */
+ PINCH_AND_PAN
+ }
+
+ private static @NonNull EditorElement createRoot(@NonNull CropStyle cropStyle) {
EditorElement root = new EditorElement(null);
EditorElement imageRoot = new EditorElement(null);
@@ -96,7 +117,8 @@ final class EditorElementHierarchy {
EditorElement imageCrop = new EditorElement(null);
overlay.addElement(imageCrop);
- EditorElement cropEditorElement = new EditorElement(new CropAreaRenderer(R.color.crop_area_renderer_outer_color, !circleEdit));
+ boolean renderCenterThumbs = cropStyle == CropStyle.RECTANGLE;
+ EditorElement cropEditorElement = new EditorElement(new CropAreaRenderer(R.color.crop_area_renderer_outer_color, renderCenterThumbs));
cropEditorElement.getFlags()
.setRotateLocked(true)
@@ -116,14 +138,18 @@ final class EditorElementHierarchy {
cropEditorElement.addElement(blackout);
- cropEditorElement.addElement(createThumbs(cropEditorElement, !circleEdit));
+ if (cropStyle == CropStyle.PINCH_AND_PAN) {
+ cropEditorElement.addElement(new EditorElement(null));
+ } else {
+ cropEditorElement.addElement(createThumbs(cropEditorElement, renderCenterThumbs));
- if (circleEdit) {
- EditorElement circle = new EditorElement(new OvalGuideRenderer(R.color.crop_circle_guide_color));
- circle.getFlags().setSelectable(false)
- .persist();
+ if (cropStyle == CropStyle.CIRCLE) {
+ EditorElement circle = new EditorElement(new OvalGuideRenderer(R.color.crop_circle_guide_color));
+ circle.getFlags().setSelectable(false)
+ .persist();
- cropEditorElement.addElement(circle);
+ cropEditorElement.addElement(circle);
+ }
}
return root;
@@ -197,11 +223,14 @@ final class EditorElementHierarchy {
return flipRotate;
}
- void startCrop(@NonNull Runnable invalidate) {
- Matrix editor = new Matrix();
- float scaleInForCrop = 0.8f;
+ /**
+ * @param scaleIn Use 1 for no scale in, use less than 1 and it will zoom the image out
+ * so user can see more of the surrounding image while cropping.
+ */
+ void startCrop(@NonNull Runnable invalidate, float scaleIn) {
+ Matrix editor = new Matrix();
- editor.postScale(scaleInForCrop, scaleInForCrop);
+ editor.postScale(scaleIn, scaleIn);
root.animateEditorTo(editor, invalidate);
cropEditorElement.getFlags()
diff --git a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java
index b94b54ac62..0ed3e94802 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java
@@ -60,17 +60,21 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
private EditorElementHierarchy editorElementHierarchy;
- private final RectF visibleViewPort = new RectF();
- private final Point size;
- private final boolean circleEditing;
+ private final RectF visibleViewPort = new RectF();
+ private final Point size;
+ private final EditingPurpose editingPurpose;
+ private float fixedRatio;
- public EditorModel() {
- this(false, EditorElementHierarchy.create());
+ private enum EditingPurpose {
+ IMAGE,
+ AVATAR_CIRCLE,
+ WALLPAPER
}
private EditorModel(@NonNull Parcel in) {
ClassLoader classLoader = getClass().getClassLoader();
- this.circleEditing = in.readByte() == 1;
+ this.editingPurpose = EditingPurpose.values()[in.readInt()];
+ this.fixedRatio = in.readFloat();
this.size = new Point(in.readInt(), in.readInt());
//noinspection ConstantConditions
this.editorElementHierarchy = EditorElementHierarchy.create(in.readParcelable(classLoader));
@@ -78,8 +82,9 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
this.cropUndoRedoStacks = in.readParcelable(classLoader);
}
- public EditorModel(boolean circleEditing, @NonNull EditorElementHierarchy editorElementHierarchy) {
- this.circleEditing = circleEditing;
+ public EditorModel(@NonNull EditingPurpose editingPurpose, float fixedRatio, @NonNull EditorElementHierarchy editorElementHierarchy) {
+ this.editingPurpose = editingPurpose;
+ this.fixedRatio = fixedRatio;
this.size = new Point(1024, 1024);
this.editorElementHierarchy = editorElementHierarchy;
this.undoRedoStacks = new UndoRedoStacks(50);
@@ -87,11 +92,17 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
}
public static EditorModel create() {
- return new EditorModel(false, EditorElementHierarchy.create());
+ return new EditorModel(EditingPurpose.IMAGE, 0, EditorElementHierarchy.create());
}
public static EditorModel createForCircleEditing() {
- EditorModel editorModel = new EditorModel(true, EditorElementHierarchy.createForCircleEditing());
+ EditorModel editorModel = new EditorModel(EditingPurpose.AVATAR_CIRCLE, 1, EditorElementHierarchy.createForCircleEditing());
+ editorModel.setCropAspectLock(true);
+ return editorModel;
+ }
+
+ public static EditorModel createForWallpaperEditing(float fixedRatio) {
+ EditorModel editorModel = new EditorModel(EditingPurpose.WALLPAPER, fixedRatio, EditorElementHierarchy.createForPinchAndPanCropping());
editorModel.setCropAspectLock(true);
return editorModel;
}
@@ -260,9 +271,11 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
}
public void startCrop() {
+ float scaleIn = editingPurpose == EditingPurpose.WALLPAPER ? 1 : 0.8f;
+
pushUndoPoint();
cropUndoRedoStacks.clear(editorElementHierarchy.getRoot());
- editorElementHierarchy.startCrop(invalidate);
+ editorElementHierarchy.startCrop(invalidate, scaleIn);
inBoundsMemory.push(editorElementHierarchy.getMainImage(), editorElementHierarchy.getCropEditorElement());
updateUndoRedoAvailableState(cropUndoRedoStacks);
}
@@ -538,7 +551,8 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
@Override
public void writeToParcel(Parcel dest, int flags) {
- dest.writeByte((byte) (circleEditing ? 1 : 0));
+ dest.writeInt(editingPurpose.ordinal());
+ dest.writeFloat(fixedRatio);
dest.writeInt(size.x);
dest.writeInt(size.y);
dest.writeParcelable(editorElementHierarchy.getRoot(), flags);
@@ -628,10 +642,10 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
if (imageCropMatrix.isIdentity()) {
imageCropMatrix.set(cropMatrix);
- if (circleEditing) {
+ if (editingPurpose == EditingPurpose.AVATAR_CIRCLE || editingPurpose == EditingPurpose.WALLPAPER) {
Matrix userCropMatrix = editorElementHierarchy.getCropEditorElement().getLocalMatrix();
if (size.x > size.y) {
- userCropMatrix.setScale(size.y / (float) size.x, 1f);
+ userCropMatrix.setScale(fixedRatio * size.y / (float) size.x, 1f);
} else {
userCropMatrix.setScale(1f, size.x / (float) size.y);
}
@@ -643,13 +657,37 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
undoRedoStacks.clear(editorElementHierarchy.getRoot());
}
- if (circleEditing) {
- startCrop();
+ switch (editingPurpose) {
+ case AVATAR_CIRCLE: {
+ startCrop();
+ break;
+ }
+ case WALLPAPER: {
+ setFixedRatio(fixedRatio);
+ startCrop();
+ break;
+ }
}
}
}
}
+ public void setFixedRatio(float r) {
+ fixedRatio = r;
+ Matrix userCropMatrix = editorElementHierarchy.getCropEditorElement().getLocalMatrix();
+ float w = size.x;
+ float h = size.y;
+ float imageRatio = w / h;
+ if (imageRatio > r) {
+ userCropMatrix.setScale(r / imageRatio, 1f);
+ } else {
+ userCropMatrix.setScale(1f, imageRatio / r);
+ }
+
+ editorElementHierarchy.doneCrop(visibleViewPort, null);
+ startCrop();
+ }
+
private boolean isRendererOfMainImage(@NonNull Renderer renderer) {
EditorElement mainImage = editorElementHierarchy.getMainImage();
Renderer mainImageRenderer = mainImage != null ? mainImage.getRenderer() : null;
@@ -790,7 +828,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
return editorElementHierarchy.getRoot();
}
- public EditorElement getMainImage() {
+ public @Nullable EditorElement getMainImage() {
return editorElementHierarchy.getMainImage();
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java
index e97eb7ee67..8bd54bccfa 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java
@@ -29,13 +29,19 @@ import org.thoughtcrime.securesms.recipients.Recipient;
public class MediaPickerFolderFragment extends Fragment implements MediaPickerFolderAdapter.EventListener {
private static final String KEY_TOOLBAR_TITLE = "toolbar_title";
+ private static final String KEY_HIDE_CAMERA = "hide_camera";
private String toolbarTitle;
+ private boolean showCamera;
private MediaSendViewModel viewModel;
private Controller controller;
private GridLayoutManager layoutManager;
public static @NonNull MediaPickerFolderFragment newInstance(@NonNull Context context, @Nullable Recipient recipient) {
+ return newInstance(context, recipient, false);
+ }
+
+ public static @NonNull MediaPickerFolderFragment newInstance(@NonNull Context context, @Nullable Recipient recipient, boolean hideCamera) {
String toolbarTitle;
if (recipient != null) {
@@ -45,8 +51,13 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
toolbarTitle = "";
}
+ return newInstance(toolbarTitle, hideCamera);
+ }
+
+ public static @NonNull MediaPickerFolderFragment newInstance(@NonNull String toolbarTitle, boolean hideCamera) {
Bundle args = new Bundle();
args.putString(KEY_TOOLBAR_TITLE, toolbarTitle);
+ args.putBoolean(KEY_HIDE_CAMERA, hideCamera);
MediaPickerFolderFragment fragment = new MediaPickerFolderFragment();
fragment.setArguments(args);
@@ -60,6 +71,7 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
setHasOptionsMenu(true);
toolbarTitle = getArguments().getString(KEY_TOOLBAR_TITLE);
+ showCamera = !getArguments().getBoolean(KEY_HIDE_CAMERA);
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
}
@@ -105,16 +117,14 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
@Override
public void onPrepareOptionsMenu(@NonNull Menu menu) {
- requireActivity().getMenuInflater().inflate(R.menu.mediapicker_default, menu);
+ if (showCamera) {
+ requireActivity().getMenuInflater().inflate(R.menu.mediapicker_default, menu);
+ }
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
- switch (item.getItemId()) {
- case R.id.mediapicker_menu_camera:
- controller.onCameraSelected();
- return true;
- }
+ if (item.getItemId() == R.id.mediapicker_menu_camera) { controller.onCameraSelected(); return true; }
return false;
}
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 972dcecb26..123b858268 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java
@@ -22,9 +22,7 @@ import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.GlideApp;
-import org.thoughtcrime.securesms.util.Util;
-import java.util.ArrayList;
import java.util.List;
/**
@@ -36,10 +34,12 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
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 static final String KEY_HIDE_CAMERA = "hide_camera";
private String bucketId;
private String folderTitle;
private int maxSelection;
+ private boolean showCamera;
private MediaSendViewModel viewModel;
private MediaPickerItemAdapter adapter;
private Controller controller;
@@ -50,11 +50,16 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
}
public static MediaPickerItemFragment newInstance(@NonNull String bucketId, @NonNull String folderTitle, int maxSelection, boolean forceMultiSelect) {
+ return newInstance(bucketId, folderTitle, maxSelection, forceMultiSelect, false);
+ }
+
+ public static MediaPickerItemFragment newInstance(@NonNull String bucketId, @NonNull String folderTitle, int maxSelection, boolean forceMultiSelect, boolean hideCamera) {
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);
+ args.putBoolean(KEY_HIDE_CAMERA, hideCamera);
MediaPickerItemFragment fragment = new MediaPickerItemFragment();
fragment.setArguments(args);
@@ -70,6 +75,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
bucketId = getArguments().getString(KEY_BUCKET_ID);
folderTitle = getArguments().getString(KEY_FOLDER_TITLE);
maxSelection = getArguments().getInt(KEY_MAX_SELECTION);
+ showCamera = !getArguments().getBoolean(KEY_HIDE_CAMERA);
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
}
@@ -120,16 +126,14 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
@Override
public void onPrepareOptionsMenu(@NonNull Menu menu) {
- requireActivity().getMenuInflater().inflate(R.menu.mediapicker_default, menu);
+ if (showCamera) {
+ requireActivity().getMenuInflater().inflate(R.menu.mediapicker_default, menu);
+ }
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
- switch (item.getItemId()) {
- case R.id.mediapicker_menu_camera:
- controller.onCameraSelected();
- return true;
- }
+ if (item.getItemId() == R.id.mediapicker_menu_camera) { controller.onCameraSelected(); return true; }
return false;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java
index 97b31313df..420668e961 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java
@@ -41,13 +41,16 @@ import java.util.concurrent.ExecutionException;
*
* The image can be encrypted.
*/
-final class UriGlideRenderer implements Renderer {
+public final class UriGlideRenderer implements Renderer {
private static final String TAG = Log.tag(UriGlideRenderer.class);
private static final int PREVIEW_DIMENSION_LIMIT = 2048;
private static final int MAX_BLUR_DIMENSION = 300;
+ public static final float WEAK_BLUR = 3f;
+ public static final float STRONG_BLUR = 25f;
+
private final Uri imageUri;
private final Paint paint = new Paint();
private final Matrix imageProjectionMatrix = new Matrix();
@@ -56,16 +59,22 @@ final class UriGlideRenderer implements Renderer {
private final boolean decryptable;
private final int maxWidth;
private final int maxHeight;
+ private final float blurRadius;
- @Nullable private Bitmap bitmap;
- @Nullable private Bitmap blurredBitmap;
- @Nullable private Paint blurPaint;
+ @Nullable private Bitmap bitmap;
+ @Nullable private Bitmap blurredBitmap;
+ @Nullable private Paint blurPaint;
- UriGlideRenderer(@NonNull Uri imageUri, boolean decryptable, int maxWidth, int maxHeight) {
+ public UriGlideRenderer(@NonNull Uri imageUri, boolean decryptable, int maxWidth, int maxHeight) {
+ this(imageUri, decryptable, maxWidth, maxHeight, STRONG_BLUR);
+ }
+
+ public UriGlideRenderer(@NonNull Uri imageUri, boolean decryptable, int maxWidth, int maxHeight, float blurRadius) {
this.imageUri = imageUri;
this.decryptable = decryptable;
this.maxWidth = maxWidth;
this.maxHeight = maxHeight;
+ this.blurRadius = blurRadius;
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
paint.setDither(true);
@@ -148,7 +157,7 @@ final class UriGlideRenderer implements Renderer {
blurPaint.setMaskFilter(null);
if (blurredBitmap == null) {
- blurredBitmap = blur(bitmap, rendererContext.context);
+ blurredBitmap = blur(bitmap, rendererContext.context, blurRadius);
blurScaleMatrix.setRectToRect(new RectF(0, 0, blurredBitmap.getWidth(), blurredBitmap.getHeight()),
new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight()),
@@ -235,7 +244,7 @@ final class UriGlideRenderer implements Renderer {
return matrix;
}
- private static @NonNull Bitmap blur(Bitmap bitmap, Context context) {
+ private static @NonNull Bitmap blur(Bitmap bitmap, Context context, float blurRadius) {
Point previewSize = scaleKeepingAspectRatio(new Point(bitmap.getWidth(), bitmap.getHeight()), PREVIEW_DIMENSION_LIMIT);
Point blurSize = scaleKeepingAspectRatio(new Point(previewSize.x / 2, previewSize.y / 2 ), MAX_BLUR_DIMENSION);
Bitmap small = BitmapUtil.createScaledBitmap(bitmap, blurSize.x, blurSize.y);
@@ -247,7 +256,7 @@ final class UriGlideRenderer implements Renderer {
Allocation output = Allocation.createTyped(rs, input.getType());
ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
- script.setRadius(25f);
+ script.setRadius(blurRadius);
script.setInput(input);
script.forEach(output);
@@ -283,7 +292,8 @@ final class UriGlideRenderer implements Renderer {
return new UriGlideRenderer(Uri.parse(in.readString()),
in.readInt() == 1,
in.readInt(),
- in.readInt()
+ in.readInt(),
+ in.readFloat()
);
}
@@ -304,5 +314,6 @@ final class UriGlideRenderer implements Renderer {
dest.writeInt(decryptable ? 1 : 0);
dest.writeInt(maxWidth);
dest.writeInt(maxHeight);
+ dest.writeFloat(blurRadius);
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/BitmapUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/BitmapUtil.java
index cc2e74a075..fba9971be7 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/BitmapUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/BitmapUtil.java
@@ -10,6 +10,7 @@ import android.graphics.Rect;
import android.graphics.YuvImage;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
+import android.os.Build;
import android.util.Pair;
import androidx.annotation.NonNull;
@@ -267,6 +268,17 @@ public class BitmapUtil {
return stream.toByteArray();
}
+ public static @Nullable byte[] toWebPByteArray(@Nullable Bitmap bitmap) {
+ if (bitmap == null) return null;
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ if (Build.VERSION.SDK_INT >= 30) {
+ bitmap.compress(CompressFormat.WEBP_LOSSLESS, 100, stream);
+ } else {
+ bitmap.compress(CompressFormat.WEBP, 100, stream);
+ }
+ return stream.toByteArray();
+ }
+
public static @Nullable Bitmap fromByteArray(@Nullable byte[] bytes) {
if (bytes == null) return null;
return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperSelectionFragment.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperSelectionFragment.java
index 30b91d885e..86b4667bb3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperSelectionFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperSelectionFragment.java
@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.wallpaper;
import android.app.Activity;
import android.content.Intent;
-import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -20,11 +19,11 @@ import com.google.android.flexbox.JustifyContent;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ActivityTransitionUtil;
+import org.thoughtcrime.securesms.wallpaper.crop.WallpaperImageSelectionActivity;
public class ChatWallpaperSelectionFragment extends Fragment {
- private static final short CHOOSE_PHOTO = 1;
- private static final short PREVIEW = 2;
+ private static final short CHOOSE_WALLPAPER = 1;
private ChatWallpaperViewModel viewModel;
@@ -40,13 +39,12 @@ public class ChatWallpaperSelectionFragment extends Fragment {
FlexboxLayoutManager flexboxLayoutManager = new FlexboxLayoutManager(requireContext());
chooseFromPhotos.setOnClickListener(unused -> {
- // Navigate to photo selection (akin to what we did for profile avatar selection.)
- //startActivityForResult(..., CHOOSE_PHOTO);
+ startActivityForResult(WallpaperImageSelectionActivity.getIntent(requireContext(), viewModel.getRecipientId()), CHOOSE_WALLPAPER);
});
@SuppressWarnings("CodeBlock2Expr")
ChatWallpaperSelectionAdapter adapter = new ChatWallpaperSelectionAdapter(chatWallpaper -> {
- startActivityForResult(ChatWallpaperPreviewActivity.create(requireActivity(), chatWallpaper, viewModel.getDimInDarkTheme().getValue()), PREVIEW);
+ startActivityForResult(ChatWallpaperPreviewActivity.create(requireActivity(), chatWallpaper, viewModel.getDimInDarkTheme().getValue()), CHOOSE_WALLPAPER);
ActivityTransitionUtil.setSlideInTransition(requireActivity());
});
@@ -60,17 +58,7 @@ public class ChatWallpaperSelectionFragment extends Fragment {
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
- if (requestCode == CHOOSE_PHOTO && resultCode == Activity.RESULT_OK && data != null) {
- Uri uri = data.getData();
- if (uri == null || uri == Uri.EMPTY) {
- throw new AssertionError("Should never have an empty uri.");
- } else {
- ChatWallpaper wallpaper = ChatWallpaperFactory.create(uri);
- viewModel.setWallpaper(wallpaper);
- viewModel.saveWallpaperSelection();
- Navigation.findNavController(requireView()).popBackStack();
- }
- } else if (requestCode == PREVIEW && resultCode == Activity.RESULT_OK && data != null) {
+ if (requestCode == CHOOSE_WALLPAPER && resultCode == Activity.RESULT_OK && data != null) {
ChatWallpaper chatWallpaper = data.getParcelableExtra(ChatWallpaperPreviewActivity.EXTRA_CHAT_WALLPAPER);
viewModel.setWallpaper(chatWallpaper);
viewModel.saveWallpaperSelection();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/UriChatWallpaper.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/UriChatWallpaper.java
index 5b41df04f1..3d87445bd1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/UriChatWallpaper.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/UriChatWallpaper.java
@@ -1,19 +1,30 @@
package org.thoughtcrime.securesms.wallpaper;
+import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.widget.ImageView;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.bumptech.glide.load.DataSource;
+import com.bumptech.glide.load.engine.GlideException;
+import com.bumptech.glide.request.RequestListener;
+import com.bumptech.glide.request.target.Target;
+
+import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper;
+import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.GlideApp;
import java.util.Objects;
final class UriChatWallpaper implements ChatWallpaper, Parcelable {
+ private static final String TAG = Log.tag(UriChatWallpaper.class);
+
private final Uri uri;
private final float dimLevelInDarkTheme;
@@ -30,7 +41,20 @@ final class UriChatWallpaper implements ChatWallpaper, Parcelable {
@Override
public void loadInto(@NonNull ImageView imageView) {
GlideApp.with(imageView)
- .load(uri)
+ .load(new DecryptableStreamUriLoader.DecryptableUri(uri))
+ .addListener(new RequestListener() {
+ @Override
+ public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) {
+ Log.w(TAG, "Failed to load wallpaper " + uri);
+ return false;
+ }
+
+ @Override
+ public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) {
+ Log.i(TAG, "Loaded wallpaper " + uri);
+ return false;
+ }
+ })
.into(imageView);
}
@@ -59,7 +83,7 @@ final class UriChatWallpaper implements ChatWallpaper, Parcelable {
if (o == null || getClass() != o.getClass()) return false;
UriChatWallpaper that = (UriChatWallpaper) o;
return Float.compare(that.dimLevelInDarkTheme, dimLevelInDarkTheme) == 0 &&
- uri.equals(that.uri);
+ uri.equals(that.uri);
}
@Override
diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperCropActivity.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperCropActivity.java
new file mode 100644
index 0000000000..ba92814b59
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperCropActivity.java
@@ -0,0 +1,210 @@
+package org.thoughtcrime.securesms.wallpaper.crop;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Point;
+import android.graphics.PorterDuff;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleRes;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.widget.SwitchCompat;
+import androidx.appcompat.widget.Toolbar;
+import androidx.core.content.ContextCompat;
+import androidx.lifecycle.ViewModelProviders;
+
+import org.signal.core.util.logging.Log;
+import org.thoughtcrime.securesms.BaseActivity;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.imageeditor.ImageEditorView;
+import org.thoughtcrime.securesms.imageeditor.model.EditorElement;
+import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
+import org.thoughtcrime.securesms.imageeditor.renderers.FaceBlurRenderer;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.scribbles.UriGlideRenderer;
+import org.thoughtcrime.securesms.util.AsynchronousCallback;
+import org.thoughtcrime.securesms.util.DynamicTheme;
+import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
+import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
+import org.thoughtcrime.securesms.wallpaper.ChatWallpaperPreviewActivity;
+
+import java.util.Locale;
+import java.util.Objects;
+
+public final class WallpaperCropActivity extends BaseActivity {
+
+ private static final String TAG = Log.tag(WallpaperCropActivity.class);
+
+ private static final String EXTRA_RECIPIENT_ID = "RECIPIENT_ID";
+ private static final String EXTRA_IMAGE_URI = "IMAGE_URI";
+
+ private final DynamicTheme dynamicTheme = new DynamicWallpaperTheme();
+
+ private ImageEditorView imageEditor;
+ private WallpaperCropViewModel viewModel;
+
+ public static Intent newIntent(@NonNull Context context,
+ @Nullable RecipientId recipientId,
+ @NonNull Uri imageUri)
+ {
+ Intent intent = new Intent(context, WallpaperCropActivity.class);
+ intent.putExtra(EXTRA_RECIPIENT_ID, recipientId);
+ intent.putExtra(EXTRA_IMAGE_URI, Objects.requireNonNull(imageUri));
+ return intent;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ dynamicTheme.onCreate(this);
+ setContentView(R.layout.chat_wallpaper_crop_activity);
+
+ RecipientId recipientId = getIntent().getParcelableExtra(EXTRA_RECIPIENT_ID);
+ Uri inputImage = Objects.requireNonNull(getIntent().getParcelableExtra(EXTRA_IMAGE_URI));
+
+ Log.i(TAG, "Cropping wallpaper for " + (recipientId == null ? "default wallpaper" : recipientId));
+
+ WallpaperCropViewModel.Factory factory = new WallpaperCropViewModel.Factory(recipientId);
+ viewModel = ViewModelProviders.of(this, factory).get(WallpaperCropViewModel.class);
+
+ imageEditor = findViewById(R.id.image_editor);
+ View receivedBubble = findViewById(R.id.preview_bubble_1);
+ TextView bubble2Text = findViewById(R.id.chat_wallpaper_bubble2_text);
+ View setWallPaper = findViewById(R.id.preview_set_wallpaper);
+ SwitchCompat blur = findViewById(R.id.preview_blur);
+
+ setupImageEditor(inputImage);
+
+ setWallPaper.setOnClickListener(v -> setWallpaper());
+
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ ActionBar supportActionBar = Objects.requireNonNull(getSupportActionBar());
+ supportActionBar.setHomeAsUpIndicator(ContextCompat.getDrawable(this, R.drawable.ic_arrow_left_24));
+ supportActionBar.setDisplayHomeAsUpEnabled(true);
+
+ blur.setOnCheckedChangeListener((v, checked) -> viewModel.setBlur(checked));
+
+ viewModel.getBlur()
+ .observe(this, blurred -> {
+ setBlurred(blurred);
+ if (blurred != blur.isChecked()) {
+ blur.setChecked(blurred);
+ }
+ });
+
+ viewModel.getRecipient()
+ .observe(this, r -> {
+ if (r.getId().isUnknown()) {
+ bubble2Text.setText(R.string.WallpaperCropActivity__set_wallpaper_for_all_chats);
+ } else {
+ bubble2Text.setText(getString(R.string.WallpaperCropActivity__set_wallpaper_for_s, r.getDisplayName(this)));
+ receivedBubble.getBackground().setColorFilter(r.getColor().toConversationColor(this), PorterDuff.Mode.SRC_IN);
+ }
+ });
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ dynamicTheme.onResume(this);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ if (super.onOptionsItemSelected(item)) {
+ return true;
+ }
+
+ int itemId = item.getItemId();
+
+ if (itemId == android.R.id.home) {
+ finish();
+ return true;
+ }
+
+ return false;
+ }
+
+ private void setWallpaper() {
+ EditorModel model = imageEditor.getModel();
+
+ Point size = new Point(imageEditor.getWidth(), imageEditor.getHeight());
+
+ AlertDialog dialog = SimpleProgressDialog.show(this);
+ viewModel.render(this, model, size,
+ new AsynchronousCallback.MainThread() {
+ @Override public void onComplete(@Nullable ChatWallpaper result) {
+ dialog.dismiss();
+ setResult(RESULT_OK, new Intent().putExtra(ChatWallpaperPreviewActivity.EXTRA_CHAT_WALLPAPER, result));
+ finish();
+ }
+
+ @Override public void onError(@Nullable WallpaperCropViewModel.Error error) {
+ dialog.dismiss();
+ Toast.makeText(WallpaperCropActivity.this, R.string.WallpaperCropActivity__error_setting_wallpaper, Toast.LENGTH_SHORT).show();
+ }
+ }.toWorkerCallback());
+ }
+
+ private void setupImageEditor(@NonNull Uri imageUri) {
+ DisplayMetrics displayMetrics = new DisplayMetrics();
+ getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
+ int height = displayMetrics.heightPixels;
+ int width = displayMetrics.widthPixels;
+ float ratio = width / (float) height;
+
+ EditorModel editorModel = EditorModel.createForWallpaperEditing(ratio);
+
+ EditorElement image = new EditorElement(new UriGlideRenderer(imageUri, true, width, height, UriGlideRenderer.WEAK_BLUR));
+ image.getFlags()
+ .setSelectable(false)
+ .persist();
+
+ editorModel.addElement(image);
+
+ imageEditor.setModel(editorModel);
+
+ imageEditor.setSizeChangedListener((newWidth, newHeight) -> {
+ float newRatio = newWidth / (float) newHeight;
+ Log.i(TAG, String.format(Locale.US, "Output size (%d, %d) (ratio %.2f)", newWidth, newHeight, newRatio));
+
+ editorModel.setFixedRatio(newRatio);
+ });
+ }
+
+ private void setBlurred(boolean blurred) {
+ imageEditor.getModel().clearFaceRenderers();
+
+ if (blurred) {
+ EditorElement mainImage = imageEditor.getModel().getMainImage();
+
+ if (mainImage != null) {
+ EditorElement element = new EditorElement(new FaceBlurRenderer(), EditorModel.Z_MASK);
+
+ element.getFlags()
+ .setEditable(false)
+ .setSelectable(false)
+ .persist();
+
+ mainImage.addElement(element);
+ imageEditor.invalidate();
+ }
+ }
+ }
+
+ private static final class DynamicWallpaperTheme extends DynamicTheme {
+ protected @StyleRes int getTheme() {
+ return R.style.Signal_DayNight_WallpaperCropper;
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperCropRepository.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperCropRepository.java
new file mode 100644
index 0000000000..473ddac4a3
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperCropRepository.java
@@ -0,0 +1,49 @@
+package org.thoughtcrime.securesms.wallpaper.crop;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
+import org.signal.core.util.logging.Log;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
+import org.thoughtcrime.securesms.keyvalue.SignalStore;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
+import org.thoughtcrime.securesms.wallpaper.WallpaperStorage;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+final class WallpaperCropRepository {
+
+ private static final String TAG = Log.tag(WallpaperCropRepository.class);
+
+ @Nullable private final RecipientId recipientId;
+ private final Context context;
+
+ public WallpaperCropRepository(@Nullable RecipientId recipientId) {
+ this.context = ApplicationDependencies.getApplication();
+ this.recipientId = recipientId;
+ }
+
+ @WorkerThread
+ @NonNull ChatWallpaper setWallPaper(byte[] bytes) throws IOException {
+ try (InputStream inputStream = new ByteArrayInputStream(bytes)) {
+ ChatWallpaper wallpaper = WallpaperStorage.save(context, inputStream);
+
+ if (recipientId != null) {
+ Log.i(TAG, "Setting image wallpaper for " + recipientId);
+ DatabaseFactory.getRecipientDatabase(context).setWallpaper(recipientId, wallpaper);
+ } else {
+ Log.i(TAG, "Setting image wallpaper for default");
+ SignalStore.wallpaper().setWallpaper(context, wallpaper);
+ }
+
+ return wallpaper;
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperCropViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperCropViewModel.java
new file mode 100644
index 0000000000..656b514adb
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperCropViewModel.java
@@ -0,0 +1,98 @@
+package org.thoughtcrime.securesms.wallpaper.crop;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Transformations;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+import org.signal.core.util.concurrent.SignalExecutors;
+import org.signal.core.util.logging.Log;
+import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.util.AsynchronousCallback;
+import org.thoughtcrime.securesms.util.BitmapUtil;
+import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
+import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
+
+import java.io.IOException;
+import java.util.Objects;
+
+final class WallpaperCropViewModel extends ViewModel {
+
+ private static final String TAG = Log.tag(WallpaperCropViewModel.class);
+
+ private final @NonNull WallpaperCropRepository repository;
+ private final @NonNull MutableLiveData blur;
+ private final @NonNull LiveData recipient;
+
+ public WallpaperCropViewModel(@Nullable RecipientId recipientId,
+ @NonNull WallpaperCropRepository repository)
+ {
+ this.repository = repository;
+ this.blur = new MutableLiveData<>(false);
+ this.recipient = recipientId != null ? Recipient.live(recipientId).getLiveData() : LiveDataUtil.just(Recipient.UNKNOWN);
+ }
+
+ void render(@NonNull Context context,
+ @NonNull EditorModel model,
+ @NonNull Point size,
+ @NonNull AsynchronousCallback.WorkerThread callback)
+ {
+ SignalExecutors.BOUNDED.execute(
+ () -> {
+ Bitmap bitmap = model.render(context, size);
+ try {
+ ChatWallpaper chatWallpaper = repository.setWallPaper(BitmapUtil.toWebPByteArray(bitmap));
+ callback.onComplete(chatWallpaper);
+ } catch (IOException e) {
+ Log.w(TAG, e);
+ callback.onError(Error.SAVING);
+ } finally {
+ bitmap.recycle();
+ }
+ });
+ }
+
+ LiveData getBlur() {
+ return Transformations.distinctUntilChanged(blur);
+ }
+
+ LiveData getRecipient() {
+ return recipient;
+ }
+
+ @MainThread
+ void setBlur(boolean blur) {
+ this.blur.setValue(blur);
+ }
+
+ public static class Factory implements ViewModelProvider.Factory {
+
+ private final RecipientId recipientId;
+
+ public Factory(@Nullable RecipientId recipientId) {
+ this.recipientId = recipientId;
+ }
+
+ @Override
+ public @NonNull T create(@NonNull Class modelClass) {
+
+ WallpaperCropRepository wallpaperCropRepository = new WallpaperCropRepository(recipientId);
+
+ return Objects.requireNonNull(modelClass.cast(new WallpaperCropViewModel(recipientId, wallpaperCropRepository)));
+ }
+ }
+
+ enum Error {
+ SAVING
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperImageSelectionActivity.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperImageSelectionActivity.java
new file mode 100644
index 0000000000..cc16c1f12c
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperImageSelectionActivity.java
@@ -0,0 +1,80 @@
+package org.thoughtcrime.securesms.wallpaper.crop;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.app.AppCompatDelegate;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.mediasend.Media;
+import org.thoughtcrime.securesms.mediasend.MediaFolder;
+import org.thoughtcrime.securesms.mediasend.MediaPickerFolderFragment;
+import org.thoughtcrime.securesms.mediasend.MediaPickerItemFragment;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+
+public final class WallpaperImageSelectionActivity extends AppCompatActivity
+ implements MediaPickerFolderFragment.Controller,
+ MediaPickerItemFragment.Controller
+{
+ private static final String EXTRA_RECIPIENT_ID = "RECIPIENT_ID";
+ private static final int CROP = 901;
+
+ public static Intent getIntent(@NonNull Context context,
+ @Nullable RecipientId recipientId)
+ {
+ Intent intent = new Intent(context, WallpaperImageSelectionActivity.class);
+ intent.putExtra(EXTRA_RECIPIENT_ID, recipientId);
+ return intent;
+ }
+
+ @Override
+ protected void attachBaseContext(@NonNull Context newBase) {
+ getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
+ super.attachBaseContext(newBase);
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.wallpaper_image_selection_activity);
+
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.fragment_container, MediaPickerFolderFragment.newInstance(getString(R.string.WallpaperImageSelectionActivity__choose_wallpaper_image), true))
+ .commit();
+ }
+
+ @Override
+ public void onFolderSelected(@NonNull MediaFolder folder) {
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.fragment_container, MediaPickerItemFragment.newInstance(folder.getBucketId(), folder.getTitle(), 1, false, true))
+ .addToBackStack(null)
+ .commit();
+ }
+
+ @Override
+ public void onCameraSelected() {
+ throw new AssertionError("Unexpected, Camera disabled");
+ }
+
+ @Override
+ public void onMediaSelected(@NonNull Media media) {
+ startActivityForResult(WallpaperCropActivity.newIntent(this, getRecipientId(), media.getUri()), CROP);
+ }
+
+ private RecipientId getRecipientId() {
+ return getIntent().getParcelableExtra(EXTRA_RECIPIENT_ID);
+ }
+
+ @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (requestCode == CROP && resultCode == RESULT_OK) {
+ setResult(RESULT_OK, data);
+ finish();
+ }
+ }
+}
diff --git a/app/src/main/res/layout/chat_wallpaper_crop_activity.xml b/app/src/main/res/layout/chat_wallpaper_crop_activity.xml
new file mode 100644
index 0000000000..5fa16c4854
--- /dev/null
+++ b/app/src/main/res/layout/chat_wallpaper_crop_activity.xml
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/wallpaper_image_selection_activity.xml b/app/src/main/res/layout/wallpaper_image_selection_activity.xml
new file mode 100644
index 0000000000..1d249e3e93
--- /dev/null
+++ b/app/src/main/res/layout/wallpaper_image_selection_activity.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/app/src/main/res/values-night/dark_colors.xml b/app/src/main/res/values-night/dark_colors.xml
index 741bee1a3e..720bfd392b 100644
--- a/app/src/main/res/values-night/dark_colors.xml
+++ b/app/src/main/res/values-night/dark_colors.xml
@@ -117,4 +117,6 @@
@color/core_grey_25
@color/core_grey_75
+
+ @color/transparent_black_60
diff --git a/app/src/main/res/values-night/dark_themes.xml b/app/src/main/res/values-night/dark_themes.xml
index 69dba9a75c..89ae92def6 100644
--- a/app/src/main/res/values-night/dark_themes.xml
+++ b/app/src/main/res/values-night/dark_themes.xml
@@ -15,4 +15,6 @@
+
+
diff --git a/app/src/main/res/values/light_colors.xml b/app/src/main/res/values/light_colors.xml
index 5717ae8861..e511f2139f 100644
--- a/app/src/main/res/values/light_colors.xml
+++ b/app/src/main/res/values/light_colors.xml
@@ -117,4 +117,6 @@
@color/core_grey_90
@color/core_white
+
+ @color/transparent_white_30
diff --git a/app/src/main/res/values/light_themes.xml b/app/src/main/res/values/light_themes.xml
index 043022ea8d..7509fc8120 100644
--- a/app/src/main/res/values/light_themes.xml
+++ b/app/src/main/res/values/light_themes.xml
@@ -14,4 +14,6 @@
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 25418e175e..2a7c808ab8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -2842,6 +2842,16 @@
This wallpaper will be set for all chats
Besides those you manually override
+
+ Choose wallpaper image
+
+
+ Pinch to zoom, drag to adjust.
+ Set wallpaper for all chats.
+ Set wallpaper for %s.
+ Error setting wallpaper.
+ Blur
+
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 2237cb8e43..dc9f50916b 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -42,6 +42,25 @@
+
+
+
+