mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-09 11:18:35 +00:00
Use the image editor for avatars.
This commit is contained in:
parent
f68d99d16d
commit
240b2108f3
@ -133,7 +133,6 @@ dependencies {
|
|||||||
implementation 'com.pnikosis:materialish-progress:1.5'
|
implementation 'com.pnikosis:materialish-progress:1.5'
|
||||||
implementation 'org.greenrobot:eventbus:3.0.0'
|
implementation 'org.greenrobot:eventbus:3.0.0'
|
||||||
implementation 'pl.tajchert:waitingdots:0.1.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.melnykov:floatingactionbutton:1.3.0'
|
||||||
implementation 'com.google.zxing:android-integration:3.1.0'
|
implementation 'com.google.zxing:android-integration:3.1.0'
|
||||||
implementation 'mobi.upod:time-duration-picker:1.1.3'
|
implementation 'mobi.upod:time-duration-picker:1.1.3'
|
||||||
|
@ -404,6 +404,10 @@
|
|||||||
android:theme="@style/TextSecure.LightNoActionBar"
|
android:theme="@style/TextSecure.LightNoActionBar"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
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"
|
<activity android:name=".BlockedContactsActivity"
|
||||||
android:theme="@style/TextSecure.LightTheme"
|
android:theme="@style/TextSecure.LightTheme"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
@ -21,17 +21,9 @@ import android.app.Activity;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import android.text.TextUtils;
|
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.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
@ -42,6 +34,10 @@ import android.widget.ListView;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
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.load.engine.DiskCacheStrategy;
|
||||||
import com.bumptech.glide.request.target.SimpleTarget;
|
import com.bumptech.glide.request.target.SimpleTarget;
|
||||||
import com.bumptech.glide.request.transition.Transition;
|
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.RecipientsEditor;
|
||||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||||
|
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
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.database.ThreadDatabase;
|
||||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||||
import org.thoughtcrime.securesms.groups.GroupManager.GroupActionResult;
|
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.mms.GlideApp;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
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.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
@ -98,8 +99,9 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||||
|
|
||||||
private static final int PICK_CONTACT = 1;
|
private static final short REQUEST_CODE_SELECT_AVATAR = 26165;
|
||||||
public static final int AVATAR_SIZE = 210;
|
private static final int PICK_CONTACT = 1;
|
||||||
|
public static final int AVATAR_SIZE = 210;
|
||||||
|
|
||||||
private EditText groupName;
|
private EditText groupName;
|
||||||
private ListView lv;
|
private ListView lv;
|
||||||
@ -197,8 +199,12 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||||||
recipientsEditor.setHint(R.string.recipients_panel__add_members);
|
recipientsEditor.setHint(R.string.recipients_panel__add_members);
|
||||||
recipientsPanel.setPanelChangeListener(this);
|
recipientsPanel.setPanelChangeListener(this);
|
||||||
findViewById(R.id.contacts_button).setOnClickListener(new AddRecipientButtonListener());
|
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.setImageDrawable(getDefaultGroupAvatar());
|
||||||
avatar.setOnClickListener(view -> AvatarSelection.startAvatarSelection(this, false, false));
|
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() {
|
private void initializeExistingGroup() {
|
||||||
@ -284,7 +290,6 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||||||
@Override
|
@Override
|
||||||
public void onActivityResult(int reqCode, int resultCode, final Intent data) {
|
public void onActivityResult(int reqCode, int resultCode, final Intent data) {
|
||||||
super.onActivityResult(reqCode, resultCode, data);
|
super.onActivityResult(reqCode, resultCode, data);
|
||||||
Uri outputFile = Uri.fromFile(new File(getCacheDir(), "cropped"));
|
|
||||||
|
|
||||||
if (data == null || resultCode != Activity.RESULT_OK)
|
if (data == null || resultCode != Activity.RESULT_OK)
|
||||||
return;
|
return;
|
||||||
@ -299,15 +304,19 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
break;
|
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)
|
GlideApp.with(this)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.load(resultUri)
|
.load(decryptableUri)
|
||||||
.skipMemoryCache(true)
|
.skipMemoryCache(true)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
@ -315,7 +324,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||||||
.into(new SimpleTarget<Bitmap>() {
|
.into(new SimpleTarget<Bitmap>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResourceReady(@NonNull Bitmap resource, Transition<? super Bitmap> transition) {
|
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.Point;
|
||||||
import android.graphics.PointF;
|
import android.graphics.PointF;
|
||||||
import android.graphics.RectF;
|
import android.graphics.RectF;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ import org.thoughtcrime.securesms.R;
|
|||||||
import org.thoughtcrime.securesms.imageeditor.Bounds;
|
import org.thoughtcrime.securesms.imageeditor.Bounds;
|
||||||
import org.thoughtcrime.securesms.imageeditor.renderers.CropAreaRenderer;
|
import org.thoughtcrime.securesms.imageeditor.renderers.CropAreaRenderer;
|
||||||
import org.thoughtcrime.securesms.imageeditor.renderers.InverseFillRenderer;
|
import org.thoughtcrime.securesms.imageeditor.renderers.InverseFillRenderer;
|
||||||
|
import org.thoughtcrime.securesms.imageeditor.renderers.OvalGuideRenderer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates and handles a strict EditorElement Hierarchy.
|
* Creates and handles a strict EditorElement Hierarchy.
|
||||||
@ -43,15 +45,15 @@ import org.thoughtcrime.securesms.imageeditor.renderers.InverseFillRenderer;
|
|||||||
final class EditorElementHierarchy {
|
final class EditorElementHierarchy {
|
||||||
|
|
||||||
static @NonNull EditorElementHierarchy create() {
|
static @NonNull EditorElementHierarchy create() {
|
||||||
return new EditorElementHierarchy(createRoot());
|
return new EditorElementHierarchy(createRoot(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
static @NonNull EditorElementHierarchy create(@Nullable EditorElement root) {
|
static @NonNull EditorElementHierarchy createForCircleEditing() {
|
||||||
if (root == null) {
|
return new EditorElementHierarchy(createRoot(true));
|
||||||
return create();
|
}
|
||||||
} else {
|
|
||||||
return new EditorElementHierarchy(root);
|
static @NonNull EditorElementHierarchy create(@NonNull EditorElement root) {
|
||||||
}
|
return new EditorElementHierarchy(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
private final EditorElement root;
|
private final EditorElement root;
|
||||||
@ -76,7 +78,7 @@ final class EditorElementHierarchy {
|
|||||||
this.thumbs = this.cropEditorElement.getChild(1);
|
this.thumbs = this.cropEditorElement.getChild(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @NonNull EditorElement createRoot() {
|
private static @NonNull EditorElement createRoot(boolean circleEdit) {
|
||||||
EditorElement root = new EditorElement(null);
|
EditorElement root = new EditorElement(null);
|
||||||
|
|
||||||
EditorElement imageRoot = new EditorElement(null);
|
EditorElement imageRoot = new EditorElement(null);
|
||||||
@ -94,7 +96,7 @@ final class EditorElementHierarchy {
|
|||||||
EditorElement imageCrop = new EditorElement(null);
|
EditorElement imageCrop = new EditorElement(null);
|
||||||
overlay.addElement(imageCrop);
|
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()
|
cropEditorElement.getFlags()
|
||||||
.setRotateLocked(true)
|
.setRotateLocked(true)
|
||||||
@ -114,11 +116,20 @@ final class EditorElementHierarchy {
|
|||||||
|
|
||||||
cropEditorElement.addElement(blackout);
|
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;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @NonNull EditorElement createThumbs(EditorElement cropEditorElement) {
|
private static @NonNull EditorElement createThumbs(EditorElement cropEditorElement, boolean centerThumbs) {
|
||||||
EditorElement thumbs = new EditorElement(null);
|
EditorElement thumbs = new EditorElement(null);
|
||||||
|
|
||||||
thumbs.getFlags()
|
thumbs.getFlags()
|
||||||
@ -127,11 +138,13 @@ final class EditorElementHierarchy {
|
|||||||
.setVisible(false)
|
.setVisible(false)
|
||||||
.persist();
|
.persist();
|
||||||
|
|
||||||
thumbs.addElement(newThumb(cropEditorElement, ThumbRenderer.ControlPoint.CENTER_LEFT));
|
if (centerThumbs) {
|
||||||
thumbs.addElement(newThumb(cropEditorElement, ThumbRenderer.ControlPoint.CENTER_RIGHT));
|
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.TOP_CENTER));
|
||||||
thumbs.addElement(newThumb(cropEditorElement, ThumbRenderer.ControlPoint.BOTTOM_CENTER));
|
thumbs.addElement(newThumb(cropEditorElement, ThumbRenderer.ControlPoint.BOTTOM_CENTER));
|
||||||
|
}
|
||||||
|
|
||||||
thumbs.addElement(newThumb(cropEditorElement, ThumbRenderer.ControlPoint.TOP_LEFT));
|
thumbs.addElement(newThumb(cropEditorElement, ThumbRenderer.ControlPoint.TOP_LEFT));
|
||||||
thumbs.addElement(newThumb(cropEditorElement, ThumbRenderer.ControlPoint.TOP_RIGHT));
|
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_OUTPUT_WIDTH = 1024;
|
||||||
|
|
||||||
private static final int MINIMUM_CROP_PIXEL_COUNT = 100;
|
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
|
@NonNull
|
||||||
private Runnable invalidate = NULL_RUNNABLE;
|
private Runnable invalidate = NULL_RUNNABLE;
|
||||||
@ -50,26 +50,44 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
|||||||
|
|
||||||
private final UndoRedoStacks undoRedoStacks;
|
private final UndoRedoStacks undoRedoStacks;
|
||||||
private final UndoRedoStacks cropUndoRedoStacks;
|
private final UndoRedoStacks cropUndoRedoStacks;
|
||||||
private final InBoundsMemory inBoundsMemory = new InBoundsMemory();
|
private final InBoundsMemory inBoundsMemory = new InBoundsMemory();
|
||||||
|
|
||||||
private EditorElementHierarchy editorElementHierarchy;
|
private EditorElementHierarchy editorElementHierarchy;
|
||||||
|
|
||||||
private final RectF visibleViewPort = new RectF();
|
private final RectF visibleViewPort = new RectF();
|
||||||
private final Point size;
|
private final Point size;
|
||||||
|
private final boolean circleEditing;
|
||||||
|
|
||||||
public EditorModel() {
|
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.size = new Point(1024, 1024);
|
||||||
this.editorElementHierarchy = EditorElementHierarchy.create();
|
this.editorElementHierarchy = editorElementHierarchy;
|
||||||
this.undoRedoStacks = new UndoRedoStacks(50);
|
this.undoRedoStacks = new UndoRedoStacks(50);
|
||||||
this.cropUndoRedoStacks = new UndoRedoStacks(50);
|
this.cropUndoRedoStacks = new UndoRedoStacks(50);
|
||||||
}
|
}
|
||||||
|
|
||||||
private EditorModel(Parcel in) {
|
public static EditorModel create() {
|
||||||
ClassLoader classLoader = getClass().getClassLoader();
|
return new EditorModel(false, EditorElementHierarchy.create());
|
||||||
this.size = new Point(in.readInt(), in.readInt());
|
}
|
||||||
this.editorElementHierarchy = EditorElementHierarchy.create(in.readParcelable(classLoader));
|
|
||||||
this.undoRedoStacks = in.readParcelable(classLoader);
|
public static EditorModel createForCircleEditing() {
|
||||||
this.cropUndoRedoStacks = in.readParcelable(classLoader);
|
EditorModel editorModel = new EditorModel(true, EditorElementHierarchy.createForCircleEditing());
|
||||||
|
editorModel.setCropAspectLock(true);
|
||||||
|
return editorModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setInvalidate(@Nullable Runnable invalidate) {
|
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 outputPixelCount = outputSize.x * outputSize.y;
|
||||||
int minimumPixelCount = Math.min(size.x * size.y, MINIMUM_CROP_PIXEL_COUNT);
|
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) {
|
if (compareRatios(size, thinnestRatio) < 0) {
|
||||||
// original is narrower than the thinnestRatio
|
// original is narrower than the thinnestRatio
|
||||||
@ -514,6 +532,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeToParcel(Parcel dest, int flags) {
|
public void writeToParcel(Parcel dest, int flags) {
|
||||||
|
dest.writeByte((byte) (circleEditing ? 1 : 0));
|
||||||
dest.writeInt(size.x);
|
dest.writeInt(size.x);
|
||||||
dest.writeInt(size.y);
|
dest.writeInt(size.y);
|
||||||
dest.writeParcelable(editorElementHierarchy.getRoot(), flags);
|
dest.writeParcelable(editorElementHierarchy.getRoot(), flags);
|
||||||
@ -574,15 +593,30 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
|||||||
@Override
|
@Override
|
||||||
public void onReady(@NonNull Renderer renderer, @Nullable Matrix cropMatrix, @Nullable Point size) {
|
public void onReady(@NonNull Renderer renderer, @Nullable Matrix cropMatrix, @Nullable Point size) {
|
||||||
if (cropMatrix != null && size != null && isRendererOfMainImage(renderer)) {
|
if (cropMatrix != null && size != null && isRendererOfMainImage(renderer)) {
|
||||||
boolean changedBefore = isChanged();
|
boolean changedBefore = isChanged();
|
||||||
Matrix imageCropMatrix = editorElementHierarchy.getImageCrop().getLocalMatrix();
|
Matrix imageCropMatrix = editorElementHierarchy.getImageCrop().getLocalMatrix();
|
||||||
this.size.set(size.x, size.y);
|
this.size.set(size.x, size.y);
|
||||||
if (imageCropMatrix.isIdentity()) {
|
if (imageCropMatrix.isIdentity()) {
|
||||||
imageCropMatrix.set(cropMatrix);
|
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);
|
editorElementHierarchy.doneCrop(visibleViewPort, null);
|
||||||
|
|
||||||
if (!changedBefore) {
|
if (!changedBefore) {
|
||||||
undoRedoStacks.clear(editorElementHierarchy.getRoot());
|
undoRedoStacks.clear(editorElementHierarchy.getRoot());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (circleEditing) {
|
||||||
|
startCrop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import android.graphics.Paint;
|
|||||||
import android.graphics.Path;
|
import android.graphics.Path;
|
||||||
import android.graphics.RectF;
|
import android.graphics.RectF;
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
|
|
||||||
import androidx.annotation.ColorRes;
|
import androidx.annotation.ColorRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.core.content.res.ResourcesCompat;
|
import androidx.core.content.res.ResourcesCompat;
|
||||||
@ -25,7 +26,8 @@ import org.thoughtcrime.securesms.imageeditor.RendererContext;
|
|||||||
public final class CropAreaRenderer implements Renderer {
|
public final class CropAreaRenderer implements Renderer {
|
||||||
|
|
||||||
@ColorRes
|
@ColorRes
|
||||||
private final int color;
|
private final int color;
|
||||||
|
private final boolean renderCenterThumbs;
|
||||||
|
|
||||||
private final Path cropClipPath = new Path();
|
private final Path cropClipPath = new Path();
|
||||||
private final Path screenClipPath = 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.drawRect(-thickness, -thickness, size, size, paint);
|
||||||
|
|
||||||
canvas.translate(0, halfDy);
|
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.translate(0, halfDy);
|
||||||
canvas.drawRect(-thickness, -thickness, size, size, paint);
|
canvas.drawRect(-thickness, -thickness, size, size, paint);
|
||||||
|
|
||||||
canvas.translate(halfDx, 0);
|
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.translate(halfDx, 0);
|
||||||
canvas.drawRect(-thickness, -thickness, size, size, paint);
|
canvas.drawRect(-thickness, -thickness, size, size, paint);
|
||||||
|
|
||||||
canvas.translate(0, -halfDy);
|
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.translate(0, -halfDy);
|
||||||
canvas.drawRect(-thickness, -thickness, size, size, paint);
|
canvas.drawRect(-thickness, -thickness, size, size, paint);
|
||||||
|
|
||||||
canvas.translate(-halfDx, 0);
|
canvas.translate(-halfDx, 0);
|
||||||
canvas.drawRect(-thickness, -thickness, size, size, paint);
|
if (renderCenterThumbs) canvas.drawRect(-thickness, -thickness, size, size, paint);
|
||||||
|
|
||||||
rendererContext.restore();
|
rendererContext.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
public CropAreaRenderer(@ColorRes int color) {
|
public CropAreaRenderer(@ColorRes int color, boolean renderCenterThumbs) {
|
||||||
this.color = color;
|
this.color = color;
|
||||||
|
this.renderCenterThumbs = renderCenterThumbs;
|
||||||
|
|
||||||
cropClipPath.toggleInverseFillType();
|
cropClipPath.toggleInverseFillType();
|
||||||
cropClipPath.moveTo(Bounds.LEFT, Bounds.TOP);
|
cropClipPath.moveTo(Bounds.LEFT, Bounds.TOP);
|
||||||
cropClipPath.lineTo(Bounds.RIGHT, Bounds.TOP);
|
cropClipPath.lineTo(Bounds.RIGHT, Bounds.TOP);
|
||||||
@ -100,10 +104,6 @@ public final class CropAreaRenderer implements Renderer {
|
|||||||
screenClipPath.toggleInverseFillType();
|
screenClipPath.toggleInverseFillType();
|
||||||
}
|
}
|
||||||
|
|
||||||
private CropAreaRenderer(Parcel in) {
|
|
||||||
this(in.readInt());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean hitTest(float x, float y) {
|
public boolean hitTest(float x, float y) {
|
||||||
return !Bounds.contains(x, 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>() {
|
public static final Creator<CropAreaRenderer> CREATOR = new Creator<CropAreaRenderer>() {
|
||||||
@Override
|
@Override
|
||||||
public CropAreaRenderer createFromParcel(Parcel in) {
|
public @NonNull CropAreaRenderer createFromParcel(@NonNull Parcel in) {
|
||||||
return new CropAreaRenderer(in);
|
return new CropAreaRenderer(in.readInt(),
|
||||||
|
in.readByte() == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CropAreaRenderer[] newArray(int size) {
|
public @NonNull CropAreaRenderer[] newArray(int size) {
|
||||||
return new CropAreaRenderer[size];
|
return new CropAreaRenderer[size];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int describeContents() {
|
public void writeToParcel(Parcel dest, int flags) {
|
||||||
return 0;
|
dest.writeInt(color);
|
||||||
|
dest.writeByte((byte) (renderCenterThumbs ? 1 : 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeToParcel(Parcel dest, int flags) {
|
public int describeContents() {
|
||||||
dest.writeInt(color);
|
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;
|
package org.thoughtcrime.securesms.mediasend;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.camera.core.CameraX;
|
import androidx.camera.core.CameraX;
|
||||||
@ -10,8 +9,6 @@ import androidx.fragment.app.Fragment;
|
|||||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
|
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
|
||||||
|
|
||||||
import java.io.FileDescriptor;
|
import java.io.FileDescriptor;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public interface CameraFragment {
|
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 {
|
interface Controller {
|
||||||
void onCameraError();
|
void onCameraError();
|
||||||
void onImageCaptured(@NonNull byte[] data, int width, int height);
|
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.CameraX;
|
||||||
import androidx.camera.core.ImageCapture;
|
import androidx.camera.core.ImageCapture;
|
||||||
import androidx.camera.core.ImageProxy;
|
import androidx.camera.core.ImageProxy;
|
||||||
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
|
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
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.Stopwatch;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
import org.thoughtcrime.securesms.video.VideoUtil;
|
import org.thoughtcrime.securesms.video.VideoUtil;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
@ -60,7 +58,8 @@ import java.io.IOException;
|
|||||||
@RequiresApi(21)
|
@RequiresApi(21)
|
||||||
public class CameraXFragment extends Fragment implements CameraFragment {
|
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 CameraXView camera;
|
||||||
private ViewGroup controlsContainer;
|
private ViewGroup controlsContainer;
|
||||||
@ -69,8 +68,22 @@ public class CameraXFragment extends Fragment implements CameraFragment {
|
|||||||
private View selfieFlash;
|
private View selfieFlash;
|
||||||
private MemoryFileDescriptor videoFileDescriptor;
|
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() {
|
public static CameraXFragment newInstance() {
|
||||||
return new CameraXFragment();
|
CameraXFragment fragment = new CameraXFragment();
|
||||||
|
|
||||||
|
fragment.setArguments(new Bundle());
|
||||||
|
|
||||||
|
return fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -282,9 +295,10 @@ public class CameraXFragment extends Fragment implements CameraFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isVideoRecordingSupported(@NonNull Context context) {
|
private boolean isVideoRecordingSupported(@NonNull Context context) {
|
||||||
return Build.VERSION.SDK_INT >= 26 &&
|
return Build.VERSION.SDK_INT >= 26 &&
|
||||||
MediaConstraints.isVideoTranscodeAvailable() &&
|
requireArguments().getBoolean(IS_VIDEO_ENABLED, true) &&
|
||||||
CameraXUtil.isMixedModeSupported(context) &&
|
MediaConstraints.isVideoTranscodeAvailable() &&
|
||||||
|
CameraXUtil.isMixedModeSupported(context) &&
|
||||||
VideoUtil.getMaxVideoDurationInSeconds(context, viewModel.getMediaConstraints()) > 0;
|
VideoUtil.getMaxVideoDurationInSeconds(context, viewModel.getMediaConstraints()) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,25 +1,25 @@
|
|||||||
package org.thoughtcrime.securesms.mediasend;
|
package org.thoughtcrime.securesms.mediasend;
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.graphics.Point;
|
import android.graphics.Point;
|
||||||
import android.os.Bundle;
|
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.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.Toast;
|
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.R;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
@ -32,9 +32,10 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public class MediaPickerItemFragment extends Fragment implements MediaPickerItemAdapter.EventListener {
|
public class MediaPickerItemFragment extends Fragment implements MediaPickerItemAdapter.EventListener {
|
||||||
|
|
||||||
private static final String KEY_BUCKET_ID = "bucket_id";
|
private static final String KEY_BUCKET_ID = "bucket_id";
|
||||||
private static final String KEY_FOLDER_TITLE = "folder_title";
|
private static final String KEY_FOLDER_TITLE = "folder_title";
|
||||||
private static final String KEY_MAX_SELECTION = "max_selection";
|
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 bucketId;
|
||||||
private String folderTitle;
|
private String folderTitle;
|
||||||
@ -45,10 +46,15 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
|
|||||||
private GridLayoutManager layoutManager;
|
private GridLayoutManager layoutManager;
|
||||||
|
|
||||||
public static MediaPickerItemFragment newInstance(@NonNull String bucketId, @NonNull String folderTitle, int maxSelection) {
|
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();
|
Bundle args = new Bundle();
|
||||||
args.putString(KEY_BUCKET_ID, bucketId);
|
args.putString(KEY_BUCKET_ID, bucketId);
|
||||||
args.putString(KEY_FOLDER_TITLE, folderTitle);
|
args.putString(KEY_FOLDER_TITLE, folderTitle);
|
||||||
args.putInt(KEY_MAX_SELECTION, maxSelection);
|
args.putInt(KEY_MAX_SELECTION, maxSelection);
|
||||||
|
args.putBoolean(KEY_FORCE_MULTI_SELECT, forceMultiSelect);
|
||||||
|
|
||||||
MediaPickerItemFragment fragment = new MediaPickerItemFragment();
|
MediaPickerItemFragment fragment = new MediaPickerItemFragment();
|
||||||
fragment.setArguments(args);
|
fragment.setArguments(args);
|
||||||
@ -110,8 +116,10 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
|
|||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
viewModel.onItemPickerStarted();
|
viewModel.onItemPickerStarted();
|
||||||
adapter.setForcedMultiSelect(true);
|
if (requireArguments().getBoolean(KEY_FORCE_MULTI_SELECT)) {
|
||||||
viewModel.onMultiSelectStarted();
|
adapter.setForcedMultiSelect(true);
|
||||||
|
viewModel.onMultiSelectStarted();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -470,6 +470,10 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDoneEditing() {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onGlobalLayout() {
|
public void onGlobalLayout() {
|
||||||
hud.getRootView().getWindowVisibleDisplayFrame(visibleBounds);
|
hud.getRootView().getWindowVisibleDisplayFrame(visibleBounds);
|
||||||
|
@ -2,10 +2,8 @@ package org.thoughtcrime.securesms.profiles.edit;
|
|||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.animation.Animator;
|
import android.animation.Animator;
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
@ -31,28 +29,31 @@ import androidx.navigation.Navigation;
|
|||||||
|
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
import com.dd.CircularProgressButton;
|
import com.dd.CircularProgressButton;
|
||||||
|
import com.google.android.gms.common.util.IOUtils;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.avatar.AvatarSelection;
|
|
||||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.lock.v2.PinUtil;
|
import org.thoughtcrime.securesms.lock.v2.PinUtil;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
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.megaphone.Megaphones;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints;
|
|
||||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
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.DISPLAY_USERNAME;
|
||||||
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.EXCLUDE_SYSTEM;
|
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.EXCLUDE_SYSTEM;
|
||||||
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.NEXT_BUTTON_TEXT;
|
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 {
|
public class EditProfileFragment extends Fragment {
|
||||||
|
|
||||||
private static final String TAG = Log.tag(EditProfileFragment.class);
|
private static final String TAG = Log.tag(EditProfileFragment.class);
|
||||||
private static final String AVATAR_STATE = "avatar";
|
private static final String AVATAR_STATE = "avatar";
|
||||||
|
private static final short REQUEST_CODE_SELECT_AVATAR = 31726;
|
||||||
|
|
||||||
private Toolbar toolbar;
|
private Toolbar toolbar;
|
||||||
private View title;
|
private View title;
|
||||||
@ -77,7 +79,6 @@ public class EditProfileFragment extends Fragment {
|
|||||||
private TextView username;
|
private TextView username;
|
||||||
|
|
||||||
private Intent nextIntent;
|
private Intent nextIntent;
|
||||||
private File captureFile;
|
|
||||||
|
|
||||||
private EditProfileViewModel viewModel;
|
private EditProfileViewModel viewModel;
|
||||||
|
|
||||||
@ -151,52 +152,38 @@ public class EditProfileFragment extends Fragment {
|
|||||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
|
||||||
switch (requestCode) {
|
if (requestCode == REQUEST_CODE_SELECT_AVATAR && resultCode == RESULT_OK) {
|
||||||
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 (inputFile == null && captureFile != null) {
|
if (data != null && data.getBooleanExtra("delete", false)) {
|
||||||
inputFile = Uri.fromFile(captureFile);
|
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)) {
|
SimpleTask.run(() -> {
|
||||||
viewModel.setAvatar(null);
|
try {
|
||||||
avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_camera_solid_white_24).asDrawable(requireActivity(), getResources().getColor(R.color.grey_400)));
|
Media result = data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA);
|
||||||
} else {
|
InputStream stream = BlobProvider.getInstance().getStream(requireContext(), result.getUri());
|
||||||
AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar);
|
|
||||||
}
|
return IOUtils.readInputStreamFully(stream);
|
||||||
|
} catch (IOException ioException) {
|
||||||
|
Log.w(TAG, ioException);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
break;
|
(avatarBytes) -> {
|
||||||
case AvatarSelection.REQUEST_CODE_CROP_IMAGE:
|
if (avatarBytes != null) {
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
viewModel.setAvatar(avatarBytes);
|
||||||
SimpleTask.run(() -> {
|
GlideApp.with(EditProfileFragment.this)
|
||||||
try {
|
.load(avatarBytes)
|
||||||
BitmapUtil.ScaleResult result = BitmapUtil.createScaledBytes(requireActivity(), AvatarSelection.getResultUri(data), new ProfileMediaConstraints());
|
.skipMemoryCache(true)
|
||||||
return result.getBitmap();
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
} catch (BitmapDecodingException e) {
|
.circleCrop()
|
||||||
Log.w(TAG, e);
|
.into(avatar);
|
||||||
return null;
|
} 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() {
|
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() {
|
private void handleUpload() {
|
||||||
viewModel.submitProfile(uploadResult -> {
|
viewModel.submitProfile(uploadResult -> {
|
||||||
if (uploadResult == EditProfileRepository.UploadResult.SUCCESS) {
|
if (uploadResult == EditProfileRepository.UploadResult.SUCCESS) {
|
||||||
if (captureFile != null) {
|
|
||||||
if (!captureFile.delete()) {
|
|
||||||
Log.w(TAG, "Failed to delete capture file " + captureFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!PinUtil.shouldShowPinCreationDuringRegistration(requireContext())) {
|
if (!PinUtil.shouldShowPinCreationDuringRegistration(requireContext())) {
|
||||||
SignalStore.registrationValues().setRegistrationComplete();
|
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 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_OLD_STICKER_REQUEST_CODE = 123;
|
||||||
private static final int SELECT_NEW_STICKER_REQUEST_CODE = 124;
|
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 ImageEditorHud imageEditorHud;
|
||||||
private ImageEditorView imageEditorView;
|
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) {
|
public static ImageEditorFragment newInstance(@NonNull Uri imageUri) {
|
||||||
Bundle args = new Bundle();
|
Bundle args = new Bundle();
|
||||||
args.putParcelable(KEY_IMAGE_URI, imageUri);
|
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) {
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
|
||||||
|
boolean isAvatarMode = requireArguments().getBoolean(KEY_IS_AVATAR_MODE, false);
|
||||||
|
|
||||||
imageEditorHud = view.findViewById(R.id.scribble_hud);
|
imageEditorHud = view.findViewById(R.id.scribble_hud);
|
||||||
imageEditorView = view.findViewById(R.id.image_editor_view);
|
imageEditorView = view.findViewById(R.id.image_editor_view);
|
||||||
|
|
||||||
@ -150,12 +159,17 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (editorModel == null) {
|
if (editorModel == null) {
|
||||||
editorModel = new EditorModel();
|
editorModel = isAvatarMode ? EditorModel.createForCircleEditing() : EditorModel.create();
|
||||||
EditorElement image = new EditorElement(new UriGlideRenderer(imageUri, true, imageMaxWidth, imageMaxHeight));
|
EditorElement image = new EditorElement(new UriGlideRenderer(imageUri, true, imageMaxWidth, imageMaxHeight));
|
||||||
image.getFlags().setSelectable(false).persist();
|
image.getFlags().setSelectable(false).persist();
|
||||||
editorModel.addElement(image);
|
editorModel.addElement(image);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isAvatarMode) {
|
||||||
|
imageEditorHud.setUpForAvatarEditing();
|
||||||
|
imageEditorHud.enterMode(ImageEditorHud.Mode.CROP);
|
||||||
|
}
|
||||||
|
|
||||||
imageEditorView.setModel(editorModel);
|
imageEditorView.setModel(editorModel);
|
||||||
|
|
||||||
refreshUniqueColors();
|
refreshUniqueColors();
|
||||||
@ -381,6 +395,11 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
|||||||
controller.onRequestFullScreen(fullScreen, hideKeyboard);
|
controller.onRequestFullScreen(fullScreen, hideKeyboard);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDone() {
|
||||||
|
controller.onDoneEditing();
|
||||||
|
}
|
||||||
|
|
||||||
private void refreshUniqueColors() {
|
private void refreshUniqueColors() {
|
||||||
imageEditorHud.setColorPalette(imageEditorView.getModel().getUniqueColorsIgnoringAlpha());
|
imageEditorHud.setColorPalette(imageEditorView.getModel().getUniqueColorsIgnoringAlpha());
|
||||||
}
|
}
|
||||||
@ -439,5 +458,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
|||||||
void onTouchEventsNeeded(boolean needed);
|
void onTouchEventsNeeded(boolean needed);
|
||||||
|
|
||||||
void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard);
|
void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard);
|
||||||
|
|
||||||
|
void onDoneEditing();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ import android.view.View;
|
|||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
import androidx.annotation.MainThread;
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
@ -41,6 +40,7 @@ public final class ImageEditorHud extends LinearLayout {
|
|||||||
private View saveButton;
|
private View saveButton;
|
||||||
private View deleteButton;
|
private View deleteButton;
|
||||||
private View confirmButton;
|
private View confirmButton;
|
||||||
|
private View doneButton;
|
||||||
private VerticalSlideColorPicker colorPicker;
|
private VerticalSlideColorPicker colorPicker;
|
||||||
private RecyclerView colorPalette;
|
private RecyclerView colorPalette;
|
||||||
|
|
||||||
@ -88,6 +88,7 @@ public final class ImageEditorHud extends LinearLayout {
|
|||||||
deleteButton = findViewById(R.id.scribble_delete_button);
|
deleteButton = findViewById(R.id.scribble_delete_button);
|
||||||
confirmButton = findViewById(R.id.scribble_confirm_button);
|
confirmButton = findViewById(R.id.scribble_confirm_button);
|
||||||
colorPicker = findViewById(R.id.scribble_color_picker);
|
colorPicker = findViewById(R.id.scribble_color_picker);
|
||||||
|
doneButton = findViewById(R.id.scribble_done_button);
|
||||||
|
|
||||||
cropAspectLock.setOnClickListener(v -> {
|
cropAspectLock.setOnClickListener(v -> {
|
||||||
eventListener.onCropAspectLock(!eventListener.isCropAspectLocked());
|
eventListener.onCropAspectLock(!eventListener.isCropAspectLocked());
|
||||||
@ -123,6 +124,7 @@ public final class ImageEditorHud extends LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
allViews.add(stickerButton);
|
allViews.add(stickerButton);
|
||||||
|
allViews.add(doneButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setVisibleViewsWhenInMode(Mode mode, View... views) {
|
private void setVisibleViewsWhenInMode(Mode mode, View... views) {
|
||||||
@ -154,6 +156,20 @@ public final class ImageEditorHud extends LinearLayout {
|
|||||||
textButton.setOnClickListener(v -> setMode(Mode.TEXT));
|
textButton.setOnClickListener(v -> setMode(Mode.TEXT));
|
||||||
stickerButton.setOnClickListener(v -> setMode(Mode.INSERT_STICKER));
|
stickerButton.setOnClickListener(v -> setMode(Mode.INSERT_STICKER));
|
||||||
saveButton.setOnClickListener(v -> eventListener.onSave());
|
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) {
|
public void setColorPalette(@NonNull Set<Integer> colors) {
|
||||||
@ -266,6 +282,7 @@ public final class ImageEditorHud extends LinearLayout {
|
|||||||
void onCropAspectLock(boolean locked);
|
void onCropAspectLock(boolean locked);
|
||||||
boolean isCropAspectLocked();
|
boolean isCropAspectLocked();
|
||||||
void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard);
|
void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard);
|
||||||
|
void onDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final EventListener NULL_EVENT_LISTENER = new EventListener() {
|
private static final EventListener NULL_EVENT_LISTENER = new EventListener() {
|
||||||
@ -310,5 +327,9 @@ public final class ImageEditorHud extends LinearLayout {
|
|||||||
@Override
|
@Override
|
||||||
public void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard) {
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<merge
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
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
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -21,8 +20,8 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginEnd="10dp"
|
android:layout_marginEnd="10dp"
|
||||||
android:orientation="horizontal"
|
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
@ -57,7 +56,7 @@
|
|||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
android:padding="8dp"
|
android:padding="8dp"
|
||||||
android:src="@drawable/ic_text_32" />
|
android:src="@drawable/ic_text_32" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/scribble_draw_button"
|
android:id="@+id/scribble_draw_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@ -98,6 +97,14 @@
|
|||||||
android:padding="8dp"
|
android:padding="8dp"
|
||||||
android:src="@drawable/ic_check_circle_32" />
|
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>
|
</LinearLayout>
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker
|
<org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker
|
||||||
|
@ -31,6 +31,10 @@
|
|||||||
<attr name="conversation_list_compose_icon_tint" format="color" />
|
<attr name="conversation_list_compose_icon_tint" format="color" />
|
||||||
<attr name="conversation_list_camera_button_background" 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="kbs_splash_image" format="reference" />
|
||||||
|
|
||||||
<attr name="conversation_sent_card_background" format="reference|color"/>
|
<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_size">32dp</dimen>
|
||||||
<dimen name="crop_area_renderer_edge_thickness">2dp</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_edge_color">#ffffffff</color>
|
||||||
<color name="crop_area_renderer_outer_color">#7f000000</color>
|
<color name="crop_area_renderer_outer_color">#7f000000</color>
|
||||||
|
<color name="crop_circle_guide_color">#66FFFFFF</color>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
@ -355,6 +355,12 @@
|
|||||||
<string name="CustomDefaultPreference_using_default">Using default: %s</string>
|
<string name="CustomDefaultPreference_using_default">Using default: %s</string>
|
||||||
<string name="CustomDefaultPreference_none">None</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 -->
|
<!-- DateUtils -->
|
||||||
<string name="DateUtils_just_now">Now</string>
|
<string name="DateUtils_just_now">Now</string>
|
||||||
<string name="DateUtils_minutes_ago">%dm</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_primary">@color/core_grey_90</item>
|
||||||
<item name="message_request_text_color_secondary">@color/core_grey_60</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_audio">@drawable/ic_audio_light</item>
|
||||||
<item name="conversation_icon_attach_video">@drawable/ic_video_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_primary">@color/core_grey_05</item>
|
||||||
<item name="message_request_text_color_secondary">@color/core_grey_25</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_audio">@drawable/ic_audio_dark</item>
|
||||||
<item name="conversation_icon_attach_video">@drawable/ic_video_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',
|
['com.takisoft.fix:colorpicker:0.9.1',
|
||||||
'f5d0dbabe406a1800498ca9c1faf34db36e021d8488bf10360f29961fe3ab0d1'],
|
'f5d0dbabe406a1800498ca9c1faf34db36e021d8488bf10360f29961fe3ab0d1'],
|
||||||
|
|
||||||
['com.theartofdev.edmodo:android-image-cropper:2.8.0',
|
|
||||||
'5516ea87672e478b3d0ed5c274a7df27d22c02e66f899388f9b8bee93669e176'],
|
|
||||||
|
|
||||||
['com.tomergoldst.android:tooltips:1.0.6',
|
['com.tomergoldst.android:tooltips:1.0.6',
|
||||||
'4c56697dd1ad64b8066535c61f961a6d901e7ae5d97ae27084ba40ad620349b6'],
|
'4c56697dd1ad64b8066535c61f961a6d901e7ae5d97ae27084ba40ad620349b6'],
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user