Use the image editor for avatars.
This commit is contained in:
parent
f68d99d16d
commit
240b2108f3
26 changed files with 850 additions and 313 deletions
|
@ -133,7 +133,6 @@ dependencies {
|
|||
implementation 'com.pnikosis:materialish-progress:1.5'
|
||||
implementation 'org.greenrobot:eventbus:3.0.0'
|
||||
implementation 'pl.tajchert:waitingdots:0.1.0'
|
||||
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
|
||||
implementation 'com.melnykov:floatingactionbutton:1.3.0'
|
||||
implementation 'com.google.zxing:android-integration:3.1.0'
|
||||
implementation 'mobi.upod:time-duration-picker:1.1.3'
|
||||
|
|
|
@ -404,6 +404,10 @@
|
|||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".mediasend.AvatarSelectionActivity"
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".BlockedContactsActivity"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
|
|
@ -21,17 +21,9 @@ import android.app.Activity;
|
|||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.avatar.AvatarSelection;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
@ -42,6 +34,10 @@ import android.widget.ListView;
|
|||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.SimpleTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
|
@ -52,6 +48,7 @@ import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
|||
import org.thoughtcrime.securesms.contacts.RecipientsEditor;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
||||
|
@ -59,6 +56,11 @@ import org.thoughtcrime.securesms.database.RecipientDatabase;
|
|||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager.GroupActionResult;
|
||||
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.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
@ -73,7 +75,6 @@ import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
|||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
|
@ -98,8 +99,9 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private static final int PICK_CONTACT = 1;
|
||||
public static final int AVATAR_SIZE = 210;
|
||||
private static final short REQUEST_CODE_SELECT_AVATAR = 26165;
|
||||
private static final int PICK_CONTACT = 1;
|
||||
public static final int AVATAR_SIZE = 210;
|
||||
|
||||
private EditText groupName;
|
||||
private ListView lv;
|
||||
|
@ -197,8 +199,12 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||
recipientsEditor.setHint(R.string.recipients_panel__add_members);
|
||||
recipientsPanel.setPanelChangeListener(this);
|
||||
findViewById(R.id.contacts_button).setOnClickListener(new AddRecipientButtonListener());
|
||||
avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_group_outline_34, R.drawable.ic_group_outline_20).asDrawable(this, ContactColors.UNKNOWN_COLOR.toConversationColor(this)));
|
||||
avatar.setOnClickListener(view -> AvatarSelection.startAvatarSelection(this, false, false));
|
||||
avatar.setImageDrawable(getDefaultGroupAvatar());
|
||||
avatar.setOnClickListener(view -> AvatarSelectionBottomSheetDialogFragment.create(avatarBmp != null, false, REQUEST_CODE_SELECT_AVATAR).show(getSupportFragmentManager(), null));
|
||||
}
|
||||
|
||||
private Drawable getDefaultGroupAvatar() {
|
||||
return new ResourceContactPhoto(R.drawable.ic_group_outline_34, R.drawable.ic_group_outline_20).asDrawable(this, ContactColors.UNKNOWN_COLOR.toConversationColor(this));
|
||||
}
|
||||
|
||||
private void initializeExistingGroup() {
|
||||
|
@ -284,7 +290,6 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||
@Override
|
||||
public void onActivityResult(int reqCode, int resultCode, final Intent data) {
|
||||
super.onActivityResult(reqCode, resultCode, data);
|
||||
Uri outputFile = Uri.fromFile(new File(getCacheDir(), "cropped"));
|
||||
|
||||
if (data == null || resultCode != Activity.RESULT_OK)
|
||||
return;
|
||||
|
@ -299,15 +304,19 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||
}
|
||||
|
||||
break;
|
||||
case REQUEST_CODE_SELECT_AVATAR:
|
||||
if (data.getBooleanExtra("delete", false)) {
|
||||
avatarBmp = null;
|
||||
avatar.setImageDrawable(getDefaultGroupAvatar());
|
||||
return;
|
||||
}
|
||||
|
||||
final Media result = data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA);
|
||||
final DecryptableUri decryptableUri = new DecryptableUri(result.getUri());
|
||||
|
||||
case AvatarSelection.REQUEST_CODE_AVATAR:
|
||||
AvatarSelection.circularCropImage(this, data.getData(), outputFile, R.string.CropImageActivity_group_avatar);
|
||||
break;
|
||||
case AvatarSelection.REQUEST_CODE_CROP_IMAGE:
|
||||
final Uri resultUri = AvatarSelection.getResultUri(data);
|
||||
GlideApp.with(this)
|
||||
.asBitmap()
|
||||
.load(resultUri)
|
||||
.load(decryptableUri)
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.centerCrop()
|
||||
|
@ -315,7 +324,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||
.into(new SimpleTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, Transition<? super Bitmap> transition) {
|
||||
setAvatar(resultUri, resource);
|
||||
setAvatar(decryptableUri, resource);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,149 +0,0 @@
|
|||
package org.thoughtcrime.securesms.avatar;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.provider.MediaStore;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.theartofdev.edmodo.cropper.CropImage;
|
||||
import com.theartofdev.edmodo.cropper.CropImageView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.util.FileProviderUtil;
|
||||
import org.thoughtcrime.securesms.util.IntentUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import static android.provider.MediaStore.EXTRA_OUTPUT;
|
||||
|
||||
public final class AvatarSelection {
|
||||
|
||||
private static final String TAG = AvatarSelection.class.getSimpleName();
|
||||
|
||||
private AvatarSelection() {
|
||||
}
|
||||
|
||||
public static final int REQUEST_CODE_CROP_IMAGE = CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE;
|
||||
public static final int REQUEST_CODE_AVATAR = REQUEST_CODE_CROP_IMAGE + 1;
|
||||
|
||||
/**
|
||||
* Returns result on {@link #REQUEST_CODE_CROP_IMAGE}
|
||||
*/
|
||||
public static void circularCropImage(Activity activity, Uri inputFile, Uri outputFile, @StringRes int title) {
|
||||
CropImage.activity(inputFile)
|
||||
.setGuidelines(CropImageView.Guidelines.ON)
|
||||
.setAspectRatio(1, 1)
|
||||
.setCropShape(CropImageView.CropShape.OVAL)
|
||||
.setOutputUri(outputFile)
|
||||
.setAllowRotation(true)
|
||||
.setAllowFlipping(true)
|
||||
.setBackgroundColor(ContextCompat.getColor(activity, R.color.avatar_background))
|
||||
.setActivityTitle(activity.getString(title))
|
||||
.start(activity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns result on {@link #REQUEST_CODE_CROP_IMAGE}
|
||||
*/
|
||||
public static void circularCropImage(Fragment fragment, Uri inputFile, Uri outputFile, @StringRes int title) {
|
||||
CropImage.activity(inputFile)
|
||||
.setGuidelines(CropImageView.Guidelines.ON)
|
||||
.setAspectRatio(1, 1)
|
||||
.setCropShape(CropImageView.CropShape.OVAL)
|
||||
.setOutputUri(outputFile)
|
||||
.setAllowRotation(true)
|
||||
.setAllowFlipping(true)
|
||||
.setBackgroundColor(ContextCompat.getColor(fragment.requireContext(), R.color.avatar_background))
|
||||
.setActivityTitle(fragment.requireContext().getString(title))
|
||||
.start(fragment.requireContext(), fragment);
|
||||
}
|
||||
|
||||
public static Uri getResultUri(Intent data) {
|
||||
return CropImage.getActivityResult(data).getUri();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns result on {@link #REQUEST_CODE_AVATAR}
|
||||
*
|
||||
* @return Temporary capture file if created.
|
||||
*/
|
||||
public static File startAvatarSelection(Activity activity, boolean includeClear, boolean attemptToIncludeCamera) {
|
||||
File captureFile = attemptToIncludeCamera ? getCaptureFile(activity) : null;
|
||||
|
||||
Intent chooserIntent = createAvatarSelectionIntent(activity, captureFile, includeClear);
|
||||
activity.startActivityForResult(chooserIntent, REQUEST_CODE_AVATAR);
|
||||
return captureFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns result on {@link #REQUEST_CODE_AVATAR}
|
||||
*
|
||||
* @return Temporary capture file if created.
|
||||
*/
|
||||
public static File startAvatarSelection(Fragment fragment, boolean includeClear, boolean attemptToIncludeCamera) {
|
||||
File captureFile = attemptToIncludeCamera ? getCaptureFile(fragment.requireContext()) : null;
|
||||
|
||||
Intent chooserIntent = createAvatarSelectionIntent(fragment.requireContext(), captureFile, includeClear);
|
||||
fragment.startActivityForResult(chooserIntent, REQUEST_CODE_AVATAR);
|
||||
return captureFile;
|
||||
}
|
||||
|
||||
private static @Nullable File getCaptureFile(@NonNull Context context) {
|
||||
if (!Permissions.hasAll(context, Manifest.permission.CAMERA)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return File.createTempFile("capture", "jpg", context.getExternalCacheDir());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Intent createAvatarSelectionIntent(Context context, @Nullable File tempCaptureFile, boolean includeClear) {
|
||||
List<Intent> extraIntents = new LinkedList<>();
|
||||
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
|
||||
|
||||
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
||||
|
||||
if (!IntentUtils.isResolvable(context, galleryIntent)) {
|
||||
galleryIntent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
galleryIntent.setType("image/*");
|
||||
}
|
||||
|
||||
if (tempCaptureFile != null) {
|
||||
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
|
||||
if (cameraIntent.resolveActivity(context.getPackageManager()) != null) {
|
||||
cameraIntent.putExtra(EXTRA_OUTPUT, FileProviderUtil.getUriFor(context, tempCaptureFile));
|
||||
extraIntents.add(cameraIntent);
|
||||
}
|
||||
}
|
||||
|
||||
if (includeClear) {
|
||||
extraIntents.add(new Intent("org.thoughtcrime.securesms.action.CLEAR_PROFILE_PHOTO"));
|
||||
}
|
||||
|
||||
Intent chooserIntent = Intent.createChooser(galleryIntent, context.getString(R.string.CreateProfileActivity_profile_photo));
|
||||
|
||||
if (!extraIntents.isEmpty()) {
|
||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Intent[0]));
|
||||
}
|
||||
|
||||
return chooserIntent;
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import android.graphics.Matrix;
|
|||
import android.graphics.Point;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.RectF;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
|
@ -11,6 +12,7 @@ import org.thoughtcrime.securesms.R;
|
|||
import org.thoughtcrime.securesms.imageeditor.Bounds;
|
||||
import org.thoughtcrime.securesms.imageeditor.renderers.CropAreaRenderer;
|
||||
import org.thoughtcrime.securesms.imageeditor.renderers.InverseFillRenderer;
|
||||
import org.thoughtcrime.securesms.imageeditor.renderers.OvalGuideRenderer;
|
||||
|
||||
/**
|
||||
* Creates and handles a strict EditorElement Hierarchy.
|
||||
|
@ -43,15 +45,15 @@ import org.thoughtcrime.securesms.imageeditor.renderers.InverseFillRenderer;
|
|||
final class EditorElementHierarchy {
|
||||
|
||||
static @NonNull EditorElementHierarchy create() {
|
||||
return new EditorElementHierarchy(createRoot());
|
||||
return new EditorElementHierarchy(createRoot(false));
|
||||
}
|
||||
|
||||
static @NonNull EditorElementHierarchy create(@Nullable EditorElement root) {
|
||||
if (root == null) {
|
||||
return create();
|
||||
} else {
|
||||
return new EditorElementHierarchy(root);
|
||||
}
|
||||
static @NonNull EditorElementHierarchy createForCircleEditing() {
|
||||
return new EditorElementHierarchy(createRoot(true));
|
||||
}
|
||||
|
||||
static @NonNull EditorElementHierarchy create(@NonNull EditorElement root) {
|
||||
return new EditorElementHierarchy(root);
|
||||
}
|
||||
|
||||
private final EditorElement root;
|
||||
|
@ -76,7 +78,7 @@ final class EditorElementHierarchy {
|
|||
this.thumbs = this.cropEditorElement.getChild(1);
|
||||
}
|
||||
|
||||
private static @NonNull EditorElement createRoot() {
|
||||
private static @NonNull EditorElement createRoot(boolean circleEdit) {
|
||||
EditorElement root = new EditorElement(null);
|
||||
|
||||
EditorElement imageRoot = new EditorElement(null);
|
||||
|
@ -94,7 +96,7 @@ final class EditorElementHierarchy {
|
|||
EditorElement imageCrop = new EditorElement(null);
|
||||
overlay.addElement(imageCrop);
|
||||
|
||||
EditorElement cropEditorElement = new EditorElement(new CropAreaRenderer(R.color.crop_area_renderer_outer_color));
|
||||
EditorElement cropEditorElement = new EditorElement(new CropAreaRenderer(R.color.crop_area_renderer_outer_color, !circleEdit));
|
||||
|
||||
cropEditorElement.getFlags()
|
||||
.setRotateLocked(true)
|
||||
|
@ -114,11 +116,20 @@ final class EditorElementHierarchy {
|
|||
|
||||
cropEditorElement.addElement(blackout);
|
||||
|
||||
cropEditorElement.addElement(createThumbs(cropEditorElement));
|
||||
cropEditorElement.addElement(createThumbs(cropEditorElement, !circleEdit));
|
||||
|
||||
if (circleEdit) {
|
||||
EditorElement circle = new EditorElement(new OvalGuideRenderer(R.color.crop_circle_guide_color));
|
||||
circle.getFlags().setSelectable(false)
|
||||
.persist();
|
||||
|
||||
cropEditorElement.addElement(circle);
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private static @NonNull EditorElement createThumbs(EditorElement cropEditorElement) {
|
||||
private static @NonNull EditorElement createThumbs(EditorElement cropEditorElement, boolean centerThumbs) {
|
||||
EditorElement thumbs = new EditorElement(null);
|
||||
|
||||
thumbs.getFlags()
|
||||
|
@ -127,11 +138,13 @@ final class EditorElementHierarchy {
|
|||
.setVisible(false)
|
||||
.persist();
|
||||
|
||||
thumbs.addElement(newThumb(cropEditorElement, ThumbRenderer.ControlPoint.CENTER_LEFT));
|
||||
thumbs.addElement(newThumb(cropEditorElement, ThumbRenderer.ControlPoint.CENTER_RIGHT));
|
||||
if (centerThumbs) {
|
||||
thumbs.addElement(newThumb(cropEditorElement, ThumbRenderer.ControlPoint.CENTER_LEFT));
|
||||
thumbs.addElement(newThumb(cropEditorElement, ThumbRenderer.ControlPoint.CENTER_RIGHT));
|
||||
|
||||
thumbs.addElement(newThumb(cropEditorElement, ThumbRenderer.ControlPoint.TOP_CENTER));
|
||||
thumbs.addElement(newThumb(cropEditorElement, ThumbRenderer.ControlPoint.BOTTOM_CENTER));
|
||||
thumbs.addElement(newThumb(cropEditorElement, ThumbRenderer.ControlPoint.TOP_CENTER));
|
||||
thumbs.addElement(newThumb(cropEditorElement, ThumbRenderer.ControlPoint.BOTTOM_CENTER));
|
||||
}
|
||||
|
||||
thumbs.addElement(newThumb(cropEditorElement, ThumbRenderer.ControlPoint.TOP_LEFT));
|
||||
thumbs.addElement(newThumb(cropEditorElement, ThumbRenderer.ControlPoint.TOP_RIGHT));
|
||||
|
|
|
@ -41,7 +41,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
|||
private static final int MINIMUM_OUTPUT_WIDTH = 1024;
|
||||
|
||||
private static final int MINIMUM_CROP_PIXEL_COUNT = 100;
|
||||
private static final Point MINIMIM_RATIO = new Point(15, 1);
|
||||
private static final Point MINIMUM_RATIO = new Point(15, 1);
|
||||
|
||||
@NonNull
|
||||
private Runnable invalidate = NULL_RUNNABLE;
|
||||
|
@ -50,26 +50,44 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
|||
|
||||
private final UndoRedoStacks undoRedoStacks;
|
||||
private final UndoRedoStacks cropUndoRedoStacks;
|
||||
private final InBoundsMemory inBoundsMemory = new InBoundsMemory();
|
||||
private final InBoundsMemory inBoundsMemory = new InBoundsMemory();
|
||||
|
||||
private EditorElementHierarchy editorElementHierarchy;
|
||||
|
||||
private final RectF visibleViewPort = new RectF();
|
||||
private final Point size;
|
||||
private final RectF visibleViewPort = new RectF();
|
||||
private final Point size;
|
||||
private final boolean circleEditing;
|
||||
|
||||
public EditorModel() {
|
||||
this(false, EditorElementHierarchy.create());
|
||||
}
|
||||
|
||||
private EditorModel(@NonNull Parcel in) {
|
||||
ClassLoader classLoader = getClass().getClassLoader();
|
||||
this.circleEditing = in.readByte() == 1;
|
||||
this.size = new Point(in.readInt(), in.readInt());
|
||||
//noinspection ConstantConditions
|
||||
this.editorElementHierarchy = EditorElementHierarchy.create(in.readParcelable(classLoader));
|
||||
this.undoRedoStacks = in.readParcelable(classLoader);
|
||||
this.cropUndoRedoStacks = in.readParcelable(classLoader);
|
||||
}
|
||||
|
||||
public EditorModel(boolean circleEditing, @NonNull EditorElementHierarchy editorElementHierarchy) {
|
||||
this.circleEditing = circleEditing;
|
||||
this.size = new Point(1024, 1024);
|
||||
this.editorElementHierarchy = EditorElementHierarchy.create();
|
||||
this.editorElementHierarchy = editorElementHierarchy;
|
||||
this.undoRedoStacks = new UndoRedoStacks(50);
|
||||
this.cropUndoRedoStacks = new UndoRedoStacks(50);
|
||||
}
|
||||
|
||||
private EditorModel(Parcel in) {
|
||||
ClassLoader classLoader = getClass().getClassLoader();
|
||||
this.size = new Point(in.readInt(), in.readInt());
|
||||
this.editorElementHierarchy = EditorElementHierarchy.create(in.readParcelable(classLoader));
|
||||
this.undoRedoStacks = in.readParcelable(classLoader);
|
||||
this.cropUndoRedoStacks = in.readParcelable(classLoader);
|
||||
public static EditorModel create() {
|
||||
return new EditorModel(false, EditorElementHierarchy.create());
|
||||
}
|
||||
|
||||
public static EditorModel createForCircleEditing() {
|
||||
EditorModel editorModel = new EditorModel(true, EditorElementHierarchy.createForCircleEditing());
|
||||
editorModel.setCropAspectLock(true);
|
||||
return editorModel;
|
||||
}
|
||||
|
||||
public void setInvalidate(@Nullable Runnable invalidate) {
|
||||
|
@ -427,7 +445,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
|||
int outputPixelCount = outputSize.x * outputSize.y;
|
||||
int minimumPixelCount = Math.min(size.x * size.y, MINIMUM_CROP_PIXEL_COUNT);
|
||||
|
||||
Point thinnestRatio = MINIMIM_RATIO;
|
||||
Point thinnestRatio = MINIMUM_RATIO;
|
||||
|
||||
if (compareRatios(size, thinnestRatio) < 0) {
|
||||
// original is narrower than the thinnestRatio
|
||||
|
@ -514,6 +532,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
|||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeByte((byte) (circleEditing ? 1 : 0));
|
||||
dest.writeInt(size.x);
|
||||
dest.writeInt(size.y);
|
||||
dest.writeParcelable(editorElementHierarchy.getRoot(), flags);
|
||||
|
@ -574,15 +593,30 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
|||
@Override
|
||||
public void onReady(@NonNull Renderer renderer, @Nullable Matrix cropMatrix, @Nullable Point size) {
|
||||
if (cropMatrix != null && size != null && isRendererOfMainImage(renderer)) {
|
||||
boolean changedBefore = isChanged();
|
||||
Matrix imageCropMatrix = editorElementHierarchy.getImageCrop().getLocalMatrix();
|
||||
boolean changedBefore = isChanged();
|
||||
Matrix imageCropMatrix = editorElementHierarchy.getImageCrop().getLocalMatrix();
|
||||
this.size.set(size.x, size.y);
|
||||
if (imageCropMatrix.isIdentity()) {
|
||||
imageCropMatrix.set(cropMatrix);
|
||||
|
||||
if (circleEditing) {
|
||||
Matrix userCropMatrix = editorElementHierarchy.getCropEditorElement().getLocalMatrix();
|
||||
if (size.x > size.y) {
|
||||
userCropMatrix.setScale(size.y / (float) size.x, 1f);
|
||||
} else {
|
||||
userCropMatrix.setScale(1f, size.x / (float) size.y);
|
||||
}
|
||||
}
|
||||
|
||||
editorElementHierarchy.doneCrop(visibleViewPort, null);
|
||||
|
||||
if (!changedBefore) {
|
||||
undoRedoStacks.clear(editorElementHierarchy.getRoot());
|
||||
}
|
||||
|
||||
if (circleEditing) {
|
||||
startCrop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.graphics.Paint;
|
|||
import android.graphics.Path;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.ColorRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
|
@ -25,7 +26,8 @@ import org.thoughtcrime.securesms.imageeditor.RendererContext;
|
|||
public final class CropAreaRenderer implements Renderer {
|
||||
|
||||
@ColorRes
|
||||
private final int color;
|
||||
private final int color;
|
||||
private final boolean renderCenterThumbs;
|
||||
|
||||
private final Path cropClipPath = new Path();
|
||||
private final Path screenClipPath = new Path();
|
||||
|
@ -66,31 +68,33 @@ public final class CropAreaRenderer implements Renderer {
|
|||
canvas.drawRect(-thickness, -thickness, size, size, paint);
|
||||
|
||||
canvas.translate(0, halfDy);
|
||||
canvas.drawRect(-thickness, -thickness, size, size, paint);
|
||||
if (renderCenterThumbs) canvas.drawRect(-thickness, -thickness, size, size, paint);
|
||||
|
||||
canvas.translate(0, halfDy);
|
||||
canvas.drawRect(-thickness, -thickness, size, size, paint);
|
||||
|
||||
canvas.translate(halfDx, 0);
|
||||
canvas.drawRect(-thickness, -thickness, size, size, paint);
|
||||
if (renderCenterThumbs) canvas.drawRect(-thickness, -thickness, size, size, paint);
|
||||
|
||||
canvas.translate(halfDx, 0);
|
||||
canvas.drawRect(-thickness, -thickness, size, size, paint);
|
||||
|
||||
canvas.translate(0, -halfDy);
|
||||
canvas.drawRect(-thickness, -thickness, size, size, paint);
|
||||
if (renderCenterThumbs) canvas.drawRect(-thickness, -thickness, size, size, paint);
|
||||
|
||||
canvas.translate(0, -halfDy);
|
||||
canvas.drawRect(-thickness, -thickness, size, size, paint);
|
||||
|
||||
canvas.translate(-halfDx, 0);
|
||||
canvas.drawRect(-thickness, -thickness, size, size, paint);
|
||||
if (renderCenterThumbs) canvas.drawRect(-thickness, -thickness, size, size, paint);
|
||||
|
||||
rendererContext.restore();
|
||||
}
|
||||
|
||||
public CropAreaRenderer(@ColorRes int color) {
|
||||
this.color = color;
|
||||
public CropAreaRenderer(@ColorRes int color, boolean renderCenterThumbs) {
|
||||
this.color = color;
|
||||
this.renderCenterThumbs = renderCenterThumbs;
|
||||
|
||||
cropClipPath.toggleInverseFillType();
|
||||
cropClipPath.moveTo(Bounds.LEFT, Bounds.TOP);
|
||||
cropClipPath.lineTo(Bounds.RIGHT, Bounds.TOP);
|
||||
|
@ -100,10 +104,6 @@ public final class CropAreaRenderer implements Renderer {
|
|||
screenClipPath.toggleInverseFillType();
|
||||
}
|
||||
|
||||
private CropAreaRenderer(Parcel in) {
|
||||
this(in.readInt());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hitTest(float x, float y) {
|
||||
return !Bounds.contains(x, y);
|
||||
|
@ -111,23 +111,25 @@ public final class CropAreaRenderer implements Renderer {
|
|||
|
||||
public static final Creator<CropAreaRenderer> CREATOR = new Creator<CropAreaRenderer>() {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}.
|
||||
* <p>
|
||||
* 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<OvalGuideRenderer> CREATOR = new Creator<OvalGuideRenderer>() {
|
||||
@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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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<SelectionOption> 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<SelectionOption> 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<Integer> 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<SelectionOptionViewHolder> {
|
||||
|
||||
private final List<SelectionOption> options;
|
||||
private final Consumer<SelectionOption> onOptionClicked;
|
||||
|
||||
private SelectionOptionAdapter(@NonNull List<SelectionOption> options, @NonNull Consumer<SelectionOption> 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -470,6 +470,10 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDoneEditing() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
hud.getRootView().getWindowVisibleDisplayFrame(visibleBounds);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Integer> 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() {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
5
app/src/main/res/layout/avatar_selection_activity.xml
Normal file
5
app/src/main/res/layout/avatar_selection_activity.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.AppCompat.Title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:text="@string/AvatarSelectionBottomSheetDialogFragment__choose_photo" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/avatar_selection_bottom_sheet_dialog_fragment_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:overScrollMode="never"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||
</LinearLayout>
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.appcompat.widget.AppCompatTextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/avatar_selection_bottom_sheet_dialog_fragment_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?listPreferredItemHeight"
|
||||
android:drawablePadding="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:textSize="16sp"
|
||||
app:drawableTint="?icon_tint" />
|
|
@ -1,13 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:parentTag="android.widget.LinearLayout"
|
||||
tools:background="@color/core_grey_60">
|
||||
tools:background="@color/core_grey_60"
|
||||
tools:parentTag="android.widget.LinearLayout">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
|
@ -21,8 +20,8 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
|
@ -57,7 +56,7 @@
|
|||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_text_32" />
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/scribble_draw_button"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -98,6 +97,14 @@
|
|||
android:padding="8dp"
|
||||
android:src="@drawable/ic_check_circle_32" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/scribble_done_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="8dp"
|
||||
app:srcCompat="@drawable/ic_check_circle_32" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker
|
||||
|
|
|
@ -31,6 +31,10 @@
|
|||
<attr name="conversation_list_compose_icon_tint" format="color" />
|
||||
<attr name="conversation_list_camera_button_background" format="color"/>
|
||||
|
||||
<attr name="avatar_selection_take_photo" format="reference" />
|
||||
<attr name="avatar_selection_pick_photo" format="reference" />
|
||||
<attr name="avatar_selection_remove_photo" format="reference" />
|
||||
|
||||
<attr name="kbs_splash_image" format="reference" />
|
||||
|
||||
<attr name="conversation_sent_card_background" format="reference|color"/>
|
||||
|
|
|
@ -3,8 +3,10 @@
|
|||
|
||||
<dimen name="crop_area_renderer_edge_size">32dp</dimen>
|
||||
<dimen name="crop_area_renderer_edge_thickness">2dp</dimen>
|
||||
<dimen name="oval_guide_stroke_width">1dp</dimen>
|
||||
|
||||
<color name="crop_area_renderer_edge_color">#ffffffff</color>
|
||||
<color name="crop_area_renderer_outer_color">#7f000000</color>
|
||||
<color name="crop_circle_guide_color">#66FFFFFF</color>
|
||||
|
||||
</resources>
|
|
@ -355,6 +355,12 @@
|
|||
<string name="CustomDefaultPreference_using_default">Using default: %s</string>
|
||||
<string name="CustomDefaultPreference_none">None</string>
|
||||
|
||||
<!-- AvatarSelectionBottomSheetDialogFragment -->
|
||||
<string name="AvatarSelectionBottomSheetDialogFragment__choose_photo">Choose photo</string>
|
||||
<string name="AvatarSelectionBottomSheetDialogFragment__take_photo">Take photo</string>
|
||||
<string name="AvatarSelectionBottomSheetDialogFragment__choose_from_gallery">Choose from gallery</string>
|
||||
<string name="AvatarSelectionBottomSheetDialogFragment__remove_photo">Remove photo</string>
|
||||
|
||||
<!-- DateUtils -->
|
||||
<string name="DateUtils_just_now">Now</string>
|
||||
<string name="DateUtils_minutes_ago">%dm</string>
|
||||
|
|
|
@ -368,6 +368,10 @@
|
|||
<item name="message_request_text_color_primary">@color/core_grey_90</item>
|
||||
<item name="message_request_text_color_secondary">@color/core_grey_60</item>
|
||||
|
||||
<item name="avatar_selection_take_photo">@drawable/ic_camera_outline_24</item>
|
||||
<item name="avatar_selection_pick_photo">@drawable/ic_photo_outline_24</item>
|
||||
<item name="avatar_selection_remove_photo">@drawable/ic_trash_outline_24</item>
|
||||
|
||||
<item name="conversation_icon_attach_audio">@drawable/ic_audio_light</item>
|
||||
<item name="conversation_icon_attach_video">@drawable/ic_video_light</item>
|
||||
|
||||
|
@ -631,6 +635,10 @@
|
|||
<item name="message_request_text_color_primary">@color/core_grey_05</item>
|
||||
<item name="message_request_text_color_secondary">@color/core_grey_25</item>
|
||||
|
||||
<item name="avatar_selection_take_photo">@drawable/ic_camera_solid_24</item>
|
||||
<item name="avatar_selection_pick_photo">@drawable/ic_photo_solid_24</item>
|
||||
<item name="avatar_selection_remove_photo">@drawable/ic_trash_solid_24</item>
|
||||
|
||||
<item name="conversation_icon_attach_audio">@drawable/ic_audio_dark</item>
|
||||
<item name="conversation_icon_attach_video">@drawable/ic_video_dark</item>
|
||||
|
||||
|
|
|
@ -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'],
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue