Move all files to natural position.

This commit is contained in:
Alan Evans
2020-01-06 10:52:48 -05:00
parent 0df36047e7
commit 9ebe920195
3016 changed files with 6 additions and 36 deletions

View File

@@ -0,0 +1,450 @@
package org.thoughtcrime.securesms.scribbles;
import android.Manifest;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.imageeditor.ColorableRenderer;
import org.thoughtcrime.securesms.imageeditor.ImageEditorView;
import org.thoughtcrime.securesms.imageeditor.Renderer;
import org.thoughtcrime.securesms.imageeditor.model.EditorElement;
import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
import org.thoughtcrime.securesms.imageeditor.renderers.MultiLineTextRenderer;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediasend.MediaSendPageFragment;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.PushMediaConstraints;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker;
import org.thoughtcrime.securesms.stickers.StickerSearchRepository;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.ParcelUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import java.io.ByteArrayOutputStream;
import static android.app.Activity.RESULT_OK;
public final class ImageEditorFragment extends Fragment implements ImageEditorHud.EventListener,
VerticalSlideColorPicker.OnColorChangeListener,
MediaSendPageFragment {
private static final String TAG = Log.tag(ImageEditorFragment.class);
private static final String KEY_IMAGE_URI = "image_uri";
private static final int SELECT_OLD_STICKER_REQUEST_CODE = 123;
private static final int SELECT_NEW_STICKER_REQUEST_CODE = 124;
private EditorModel restoredModel;
@Nullable private EditorElement currentSelection;
private int imageMaxHeight;
private int imageMaxWidth;
private ImageEditorFragmentViewModel viewModel;
public static class Data {
private final Bundle bundle;
Data(Bundle bundle) {
this.bundle = bundle;
}
public Data() {
this(new Bundle());
}
void writeModel(@NonNull EditorModel model) {
byte[] bytes = ParcelUtil.serialize(model);
bundle.putByteArray("MODEL", bytes);
}
@Nullable
public EditorModel readModel() {
byte[] bytes = bundle.getByteArray("MODEL");
if (bytes == null) {
return null;
}
return ParcelUtil.deserialize(bytes, EditorModel.CREATOR);
}
}
private Uri imageUri;
private Controller controller;
private ImageEditorHud imageEditorHud;
private ImageEditorView imageEditorView;
public static ImageEditorFragment newInstance(@NonNull Uri imageUri) {
Bundle args = new Bundle();
args.putParcelable(KEY_IMAGE_URI, imageUri);
ImageEditorFragment fragment = new ImageEditorFragment();
fragment.setArguments(args);
fragment.setUri(imageUri);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!(getActivity() instanceof Controller)) {
throw new IllegalStateException("Parent activity must implement Controller interface.");
}
controller = (Controller) getActivity();
Bundle arguments = getArguments();
if (arguments != null) {
imageUri = arguments.getParcelable(KEY_IMAGE_URI);
}
if (imageUri == null) {
throw new AssertionError("No KEY_IMAGE_URI supplied");
}
MediaConstraints mediaConstraints = new PushMediaConstraints();
imageMaxWidth = mediaConstraints.getImageMaxWidth(requireContext());
imageMaxHeight = mediaConstraints.getImageMaxHeight(requireContext());
StickerSearchRepository repository = new StickerSearchRepository(requireContext());
viewModel = ViewModelProviders.of(this, new ImageEditorFragmentViewModel.Factory(requireActivity().getApplication(), repository))
.get(ImageEditorFragmentViewModel.class);
viewModel.getStickersAvailability().observe(this, isAvailable -> imageEditorHud.setStickersAvailable(isAvailable));
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.image_editor_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
imageEditorHud = view.findViewById(R.id.scribble_hud);
imageEditorView = view.findViewById(R.id.image_editor_view);
imageEditorHud.setEventListener(this);
imageEditorView.setTapListener(selectionListener);
imageEditorView.setDrawingChangedListener(this::refreshUniqueColors);
imageEditorView.setUndoRedoStackListener(this::onUndoRedoAvailabilityChanged);
EditorModel editorModel = null;
if (restoredModel != null) {
editorModel = restoredModel;
restoredModel = null;
}
if (editorModel == null) {
editorModel = new EditorModel();
EditorElement image = new EditorElement(new UriGlideRenderer(imageUri, true, imageMaxWidth, imageMaxHeight));
image.getFlags().setSelectable(false).persist();
editorModel.addElement(image);
}
imageEditorView.setModel(editorModel);
refreshUniqueColors();
}
@Override
public void setUri(@NonNull Uri uri) {
this.imageUri = uri;
}
@NonNull
@Override
public Uri getUri() {
return imageUri;
}
@Nullable
@Override
public View getPlaybackControls() {
return null;
}
@Override
public Object saveState() {
Data data = new Data();
data.writeModel(imageEditorView.getModel());
return data;
}
@Override
public void restoreState(@NonNull Object state) {
if (state instanceof Data) {
Data data = (Data) state;
EditorModel model = data.readModel();
if (model != null) {
if (imageEditorView != null) {
imageEditorView.setModel(model);
refreshUniqueColors();
} else {
this.restoredModel = model;
}
}
} else {
Log.w(TAG, "Received a bad saved state. Received class: " + state.getClass().getName());
}
}
@Override
public void notifyHidden() {
}
private void changeEntityColor(int selectedColor) {
if (currentSelection != null) {
Renderer renderer = currentSelection.getRenderer();
if (renderer instanceof ColorableRenderer) {
((ColorableRenderer) renderer).setColor(selectedColor);
refreshUniqueColors();
}
}
}
private void startTextEntityEditing(@NonNull EditorElement textElement, boolean selectAll) {
imageEditorView.startTextEditing(textElement, TextSecurePreferences.isIncognitoKeyboardEnabled(requireContext()), selectAll);
}
protected void addText() {
String initialText = "";
int color = imageEditorHud.getActiveColor();
MultiLineTextRenderer renderer = new MultiLineTextRenderer(initialText, color);
EditorElement element = new EditorElement(renderer);
imageEditorView.getModel().addElementCentered(element, 1);
imageEditorView.invalidate();
currentSelection = element;
startTextEntityEditing(element, true);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK && requestCode == SELECT_NEW_STICKER_REQUEST_CODE && data != null) {
final Uri uri = data.getData();
if (uri != null) {
UriGlideRenderer renderer = new UriGlideRenderer(uri, true, imageMaxWidth, imageMaxHeight);
EditorElement element = new EditorElement(renderer);
imageEditorView.getModel().addElementCentered(element, 0.2f);
currentSelection = element;
imageEditorHud.setMode(ImageEditorHud.Mode.MOVE_DELETE);
}
} else if (resultCode == RESULT_OK && requestCode == SELECT_OLD_STICKER_REQUEST_CODE && data != null) {
final Uri uri = data.getData();
if (uri != null) {
UriGlideRenderer renderer = new UriGlideRenderer(uri, false, imageMaxWidth, imageMaxHeight);
EditorElement element = new EditorElement(renderer);
imageEditorView.getModel().addElementCentered(element, 0.2f);
currentSelection = element;
imageEditorHud.setMode(ImageEditorHud.Mode.MOVE_DELETE);
}
} else {
imageEditorHud.setMode(ImageEditorHud.Mode.NONE);
}
}
@Override
public void onModeStarted(@NonNull ImageEditorHud.Mode mode) {
imageEditorView.setMode(ImageEditorView.Mode.MoveAndResize);
imageEditorView.doneTextEditing();
controller.onTouchEventsNeeded(mode != ImageEditorHud.Mode.NONE);
switch (mode) {
case CROP: {
imageEditorView.getModel().startCrop();
break;
}
case DRAW: {
imageEditorView.startDrawing(0.01f, Paint.Cap.ROUND);
break;
}
case HIGHLIGHT: {
imageEditorView.startDrawing(0.03f, Paint.Cap.SQUARE);
break;
}
case TEXT: {
addText();
break;
}
case INSERT_STICKER: {
Intent intent = new Intent(getContext(), ImageEditorStickerSelectActivity.class);
startActivityForResult(intent, SELECT_NEW_STICKER_REQUEST_CODE);
break;
}
case MOVE_DELETE:
break;
case NONE: {
imageEditorView.getModel().doneCrop();
currentSelection = null;
break;
}
}
}
@Override
public void onColorChange(int color) {
imageEditorView.setDrawingBrushColor(color);
changeEntityColor(color);
}
@Override
public void onUndo() {
imageEditorView.getModel().undo();
refreshUniqueColors();
}
@Override
public void onDelete() {
imageEditorView.deleteElement(currentSelection);
refreshUniqueColors();
}
@Override
public void onSave() {
SaveAttachmentTask.showWarningDialog(requireContext(), (dialogInterface, i) -> {
Permissions.with(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
.onAnyDenied(() -> Toast.makeText(requireContext(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
.onAllGranted(() -> {
SimpleTask.run(() -> {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Bitmap image = imageEditorView.getModel().render(requireContext());
image.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
return BlobProvider.getInstance()
.forData(outputStream.toByteArray())
.withMimeType(MediaUtil.IMAGE_JPEG)
.createForSingleUseInMemory();
}, uri -> {
SaveAttachmentTask saveTask = new SaveAttachmentTask(requireContext());
SaveAttachmentTask.Attachment attachment = new SaveAttachmentTask.Attachment(uri, MediaUtil.IMAGE_JPEG, System.currentTimeMillis(), null);
saveTask.executeOnExecutor(SignalExecutors.BOUNDED, attachment);
});
})
.execute();
});
}
@Override
public void onFlipHorizontal() {
imageEditorView.getModel().flipHorizontal();
}
@Override
public void onRotate90AntiClockwise() {
imageEditorView.getModel().rotate90anticlockwise();
}
@Override
public void onCropAspectLock(boolean locked) {
imageEditorView.getModel().setCropAspectLock(locked);
}
@Override
public boolean isCropAspectLocked() {
return imageEditorView.getModel().isCropAspectLocked();
}
@Override
public void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard) {
controller.onRequestFullScreen(fullScreen, hideKeyboard);
}
private void refreshUniqueColors() {
imageEditorHud.setColorPalette(imageEditorView.getModel().getUniqueColorsIgnoringAlpha());
}
private void onUndoRedoAvailabilityChanged(boolean undoAvailable, boolean redoAvailable) {
imageEditorHud.setUndoAvailability(undoAvailable);
}
private final ImageEditorView.TapListener selectionListener = new ImageEditorView.TapListener() {
@Override
public void onEntityDown(@Nullable EditorElement editorElement) {
if (editorElement != null) {
controller.onTouchEventsNeeded(true);
} else {
currentSelection = null;
controller.onTouchEventsNeeded(false);
imageEditorHud.setMode(ImageEditorHud.Mode.NONE);
}
}
@Override
public void onEntitySingleTap(@Nullable EditorElement editorElement) {
currentSelection = editorElement;
if (currentSelection != null) {
if (editorElement.getRenderer() instanceof MultiLineTextRenderer) {
setTextElement(editorElement, (ColorableRenderer) editorElement.getRenderer(), imageEditorView.isTextEditing());
} else {
imageEditorHud.setMode(ImageEditorHud.Mode.MOVE_DELETE);
}
}
}
@Override
public void onEntityDoubleTap(@NonNull EditorElement editorElement) {
currentSelection = editorElement;
if (editorElement.getRenderer() instanceof MultiLineTextRenderer) {
setTextElement(editorElement, (ColorableRenderer) editorElement.getRenderer(), true);
}
}
private void setTextElement(@NonNull EditorElement editorElement,
@NonNull ColorableRenderer colorableRenderer,
boolean startEditing)
{
int color = colorableRenderer.getColor();
imageEditorHud.enterMode(ImageEditorHud.Mode.TEXT);
imageEditorHud.setActiveColor(color);
if (startEditing) {
startTextEntityEditing(editorElement, false);
}
}
};
public interface Controller {
void onTouchEventsNeeded(boolean needed);
void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard);
}
}

View File

@@ -0,0 +1,65 @@
package org.thoughtcrime.securesms.scribbles;
import android.app.Application;
import android.database.ContentObserver;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.thoughtcrime.securesms.stickers.StickerSearchRepository;
import org.thoughtcrime.securesms.util.Throttler;
public final class ImageEditorFragmentViewModel extends ViewModel {
private final Application application;
private final StickerSearchRepository repository;
private final MutableLiveData<Boolean> stickersAvailable;
private final Throttler availabilityThrottler;
private final ContentObserver packObserver;
private ImageEditorFragmentViewModel(@NonNull Application application, @NonNull StickerSearchRepository repository) {
this.application = application;
this.repository = repository;
this.stickersAvailable = new MutableLiveData<>();
this.availabilityThrottler = new Throttler(500);
this.packObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
availabilityThrottler.publish(() -> repository.getStickerFeatureAvailability(stickersAvailable::postValue));
}
};
application.getContentResolver().registerContentObserver(DatabaseContentProviders.StickerPack.CONTENT_URI, true, packObserver);
}
@NonNull LiveData<Boolean> getStickersAvailability() {
repository.getStickerFeatureAvailability(stickersAvailable::postValue);
return stickersAvailable;
}
@Override
protected void onCleared() {
application.getContentResolver().unregisterContentObserver(packObserver);
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
private final Application application;
private final StickerSearchRepository repository;
public Factory(@NonNull Application application, @NonNull StickerSearchRepository repository) {
this.application = application;
this.repository = repository;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ImageEditorFragmentViewModel(application, repository));
}
}
}

View File

@@ -0,0 +1,324 @@
package org.thoughtcrime.securesms.scribbles;
import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.scribbles.widget.ColorPaletteAdapter;
import org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* The HUD (heads-up display) that contains all of the tools for interacting with
* {@link org.thoughtcrime.securesms.imageeditor.ImageEditorView}
*/
public final class ImageEditorHud extends LinearLayout {
private View cropButton;
private View cropFlipButton;
private View cropRotateButton;
private ImageView cropAspectLock;
private View drawButton;
private View highlightButton;
private View textButton;
private View stickerButton;
private View undoButton;
private View saveButton;
private View deleteButton;
private View confirmButton;
private VerticalSlideColorPicker colorPicker;
private RecyclerView colorPalette;
@NonNull
private EventListener eventListener = NULL_EVENT_LISTENER;
@Nullable
private ColorPaletteAdapter colorPaletteAdapter;
private final Map<Mode, Set<View>> visibilityModeMap = new HashMap<>();
private final Set<View> allViews = new HashSet<>();
private Mode currentMode;
private boolean undoAvailable;
public ImageEditorHud(@NonNull Context context) {
super(context);
initialize();
}
public ImageEditorHud(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initialize();
}
public ImageEditorHud(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}
private void initialize() {
inflate(getContext(), R.layout.image_editor_hud, this);
setOrientation(VERTICAL);
cropButton = findViewById(R.id.scribble_crop_button);
cropFlipButton = findViewById(R.id.scribble_crop_flip);
cropRotateButton = findViewById(R.id.scribble_crop_rotate);
cropAspectLock = findViewById(R.id.scribble_crop_aspect_lock);
colorPalette = findViewById(R.id.scribble_color_palette);
drawButton = findViewById(R.id.scribble_draw_button);
highlightButton = findViewById(R.id.scribble_highlight_button);
textButton = findViewById(R.id.scribble_text_button);
stickerButton = findViewById(R.id.scribble_sticker_button);
undoButton = findViewById(R.id.scribble_undo_button);
saveButton = findViewById(R.id.scribble_save_button);
deleteButton = findViewById(R.id.scribble_delete_button);
confirmButton = findViewById(R.id.scribble_confirm_button);
colorPicker = findViewById(R.id.scribble_color_picker);
cropAspectLock.setOnClickListener(v -> {
eventListener.onCropAspectLock(!eventListener.isCropAspectLocked());
updateCropAspectLockImage(eventListener.isCropAspectLocked());
});
initializeViews();
initializeVisibilityMap();
setMode(Mode.NONE);
}
private void updateCropAspectLockImage(boolean cropAspectLocked) {
cropAspectLock.setImageDrawable(getResources().getDrawable(cropAspectLocked ? R.drawable.ic_crop_lock_32 : R.drawable.ic_crop_unlock_32));
}
private void initializeVisibilityMap() {
setStickersAvailable(false);
setVisibleViewsWhenInMode(Mode.DRAW, confirmButton, undoButton, colorPicker, colorPalette);
setVisibleViewsWhenInMode(Mode.HIGHLIGHT, confirmButton, undoButton, colorPicker, colorPalette);
setVisibleViewsWhenInMode(Mode.TEXT, confirmButton, deleteButton, colorPicker, colorPalette);
setVisibleViewsWhenInMode(Mode.MOVE_DELETE, confirmButton, deleteButton);
setVisibleViewsWhenInMode(Mode.INSERT_STICKER, confirmButton);
setVisibleViewsWhenInMode(Mode.CROP, confirmButton, cropFlipButton, cropRotateButton, cropAspectLock, undoButton);
for (Set<View> views : visibilityModeMap.values()) {
allViews.addAll(views);
}
allViews.add(stickerButton);
}
private void setVisibleViewsWhenInMode(Mode mode, View... views) {
visibilityModeMap.put(mode, new HashSet<>(Arrays.asList(views)));
}
@MainThread
public void setStickersAvailable(boolean stickersAvailable) {
if (stickersAvailable) {
setVisibleViewsWhenInMode(Mode.NONE, drawButton, highlightButton, textButton, stickerButton, cropButton, undoButton, saveButton);
} else {
setVisibleViewsWhenInMode(Mode.NONE, drawButton, highlightButton, textButton, cropButton, undoButton, saveButton);
}
updateButtonVisibility(currentMode);
}
private void initializeViews() {
undoButton.setOnClickListener(v -> eventListener.onUndo());
deleteButton.setOnClickListener(v -> {
eventListener.onDelete();
setMode(Mode.NONE);
});
cropButton.setOnClickListener(v -> setMode(Mode.CROP));
cropFlipButton.setOnClickListener(v -> eventListener.onFlipHorizontal());
cropRotateButton.setOnClickListener(v -> eventListener.onRotate90AntiClockwise());
confirmButton.setOnClickListener(v -> setMode(Mode.NONE));
colorPaletteAdapter = new ColorPaletteAdapter();
colorPaletteAdapter.setEventListener(colorPicker::setActiveColor);
colorPalette.setLayoutManager(new LinearLayoutManager(getContext()));
colorPalette.setAdapter(colorPaletteAdapter);
drawButton.setOnClickListener(v -> setMode(Mode.DRAW));
highlightButton.setOnClickListener(v -> setMode(Mode.HIGHLIGHT));
textButton.setOnClickListener(v -> setMode(Mode.TEXT));
stickerButton.setOnClickListener(v -> setMode(Mode.INSERT_STICKER));
saveButton.setOnClickListener(v -> eventListener.onSave());
}
public void setColorPalette(@NonNull Set<Integer> colors) {
if (colorPaletteAdapter != null) {
colorPaletteAdapter.setColors(colors);
}
}
public int getActiveColor() {
return colorPicker.getActiveColor();
}
public void setActiveColor(int color) {
colorPicker.setActiveColor(color);
}
public void setEventListener(@Nullable EventListener eventListener) {
this.eventListener = eventListener != null ? eventListener : NULL_EVENT_LISTENER;
}
public void enterMode(@NonNull Mode mode) {
setMode(mode, false);
}
public void setMode(@NonNull Mode mode) {
setMode(mode, true);
}
private void setMode(@NonNull Mode mode, boolean notify) {
this.currentMode = mode;
updateButtonVisibility(mode);
switch (mode) {
case CROP: presentModeCrop(); break;
case DRAW: presentModeDraw(); break;
case HIGHLIGHT: presentModeHighlight(); break;
case TEXT: presentModeText(); break;
}
if (notify) {
eventListener.onModeStarted(mode);
}
eventListener.onRequestFullScreen(mode != Mode.NONE, mode != Mode.TEXT);
}
private void updateButtonVisibility(@NonNull Mode mode) {
Set<View> visibleButtons = visibilityModeMap.get(mode);
for (View button : allViews) {
button.setVisibility(buttonIsVisible(visibleButtons, button) ? VISIBLE : GONE);
}
}
private boolean buttonIsVisible(@Nullable Set<View> visibleButtons, @NonNull View button) {
return visibleButtons != null &&
visibleButtons.contains(button) &&
(button != undoButton || undoAvailable);
}
private void presentModeCrop() {
updateCropAspectLockImage(eventListener.isCropAspectLocked());
}
private void presentModeDraw() {
colorPicker.setOnColorChangeListener(standardOnColorChangeListener);
colorPicker.setActiveColor(Color.RED);
}
private void presentModeHighlight() {
colorPicker.setOnColorChangeListener(highlightOnColorChangeListener);
colorPicker.setActiveColor(Color.YELLOW);
}
private void presentModeText() {
colorPicker.setOnColorChangeListener(standardOnColorChangeListener);
colorPicker.setActiveColor(Color.WHITE);
}
private final VerticalSlideColorPicker.OnColorChangeListener standardOnColorChangeListener = selectedColor -> eventListener.onColorChange(selectedColor);
private final VerticalSlideColorPicker.OnColorChangeListener highlightOnColorChangeListener = selectedColor -> eventListener.onColorChange(replaceAlphaWith128(selectedColor));
private static int replaceAlphaWith128(int color) {
return color & ~0xff000000 | 0x80000000;
}
public void setUndoAvailability(boolean undoAvailable) {
this.undoAvailable = undoAvailable;
undoButton.setVisibility(buttonIsVisible(visibilityModeMap.get(currentMode), undoButton) ? VISIBLE : GONE);
}
public enum Mode {
NONE,
CROP,
TEXT,
DRAW,
HIGHLIGHT,
MOVE_DELETE,
INSERT_STICKER,
}
public interface EventListener {
void onModeStarted(@NonNull Mode mode);
void onColorChange(int color);
void onUndo();
void onDelete();
void onSave();
void onFlipHorizontal();
void onRotate90AntiClockwise();
void onCropAspectLock(boolean locked);
boolean isCropAspectLocked();
void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard);
}
private static final EventListener NULL_EVENT_LISTENER = new EventListener() {
@Override
public void onModeStarted(@NonNull Mode mode) {
}
@Override
public void onColorChange(int color) {
}
@Override
public void onUndo() {
}
@Override
public void onDelete() {
}
@Override
public void onSave() {
}
@Override
public void onFlipHorizontal() {
}
@Override
public void onRotate90AntiClockwise() {
}
@Override
public void onCropAspectLock(boolean locked) {
}
@Override
public boolean isCropAspectLocked() {
return false;
}
@Override
public void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard) {
}
};
}

View File

@@ -0,0 +1,82 @@
package org.thoughtcrime.securesms.scribbles;
import android.content.Intent;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
import org.thoughtcrime.securesms.components.emoji.MediaKeyboardProvider;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.thoughtcrime.securesms.stickers.StickerKeyboardProvider;
import org.thoughtcrime.securesms.stickers.StickerManagementActivity;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
public final class ImageEditorStickerSelectActivity extends FragmentActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.scribble_select_new_sticker_activity);
MediaKeyboard mediaKeyboard = findViewById(R.id.emoji_drawer);
mediaKeyboard.setProviders(0, new StickerKeyboardProvider(this, new StickerKeyboardProvider.StickerEventListener() {
@Override
public void onStickerSelected(@NonNull StickerRecord sticker) {
Intent intent = new Intent();
intent.setData(sticker.getUri());
setResult(RESULT_OK, intent);
SignalExecutors.BOUNDED.execute(() ->
DatabaseFactory.getStickerDatabase(getApplicationContext())
.updateStickerLastUsedTime(sticker.getRowId(), System.currentTimeMillis())
);
finish();
}
@Override
public void onStickerManagementClicked() {
startActivity(StickerManagementActivity.getIntent(ImageEditorStickerSelectActivity.this));
}
}
));
mediaKeyboard.setKeyboardListener(new MediaKeyboard.MediaKeyboardListener() {
@Override
public void onShown() {
}
@Override
public void onHidden() {
finish();
}
@Override
public void onKeyboardProviderChanged(@NonNull MediaKeyboardProvider provider) {
}
});
mediaKeyboard.show();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
}

View File

@@ -0,0 +1,208 @@
package org.thoughtcrime.securesms.scribbles;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Parcel;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import org.thoughtcrime.securesms.imageeditor.Bounds;
import org.thoughtcrime.securesms.imageeditor.Renderer;
import org.thoughtcrime.securesms.imageeditor.RendererContext;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequest;
import java.util.concurrent.ExecutionException;
/**
* Uses Glide to load an image and implements a {@link Renderer}.
*
* The image can be encrypted.
*/
final class UriGlideRenderer implements Renderer {
private static final int PREVIEW_DIMENSION_LIMIT = 2048;
private final Uri imageUri;
private final Paint paint = new Paint();
private final Matrix imageProjectionMatrix = new Matrix();
private final Matrix temp = new Matrix();
private final boolean decryptable;
private final int maxWidth;
private final int maxHeight;
@Nullable
private Bitmap bitmap;
UriGlideRenderer(@NonNull Uri imageUri, boolean decryptable, int maxWidth, int maxHeight) {
this.imageUri = imageUri;
this.decryptable = decryptable;
this.maxWidth = maxWidth;
this.maxHeight = maxHeight;
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
paint.setDither(true);
}
@Override
public void render(@NonNull RendererContext rendererContext) {
if (getBitmap() == null) {
if (rendererContext.isBlockingLoad()) {
try {
Bitmap bitmap = getBitmapGlideRequest(rendererContext.context, false).submit().get();
setBitmap(rendererContext, bitmap);
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
} else {
getBitmapGlideRequest(rendererContext.context, true).into(new CustomTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
setBitmap(rendererContext, resource);
rendererContext.invalidate.onInvalidate(UriGlideRenderer.this);
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
bitmap = null;
}
});
}
}
final Bitmap bitmap = getBitmap();
if (bitmap != null) {
rendererContext.save();
rendererContext.canvasMatrix.concat(imageProjectionMatrix);
// Units are image level pixels at this point.
int alpha = paint.getAlpha();
paint.setAlpha(rendererContext.getAlpha(alpha));
rendererContext.canvas.drawBitmap(bitmap, 0, 0, paint);
paint.setAlpha(alpha);
rendererContext.restore();
} else if (rendererContext.isBlockingLoad()) {
// If failed to load, we draw a black out, in case image was sticker positioned to cover private info.
rendererContext.canvas.drawRect(Bounds.FULL_BOUNDS, paint);
}
}
private GlideRequest<Bitmap> getBitmapGlideRequest(@NonNull Context context, boolean preview) {
int width = this.maxWidth;
int height = this.maxHeight;
if (preview) {
width = Math.min(width, PREVIEW_DIMENSION_LIMIT);
height = Math.min(height, PREVIEW_DIMENSION_LIMIT);
}
return GlideApp.with(context)
.asBitmap()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.override(width, height)
.centerInside()
.load(decryptable ? new DecryptableStreamUriLoader.DecryptableUri(imageUri) : imageUri);
}
@Override
public boolean hitTest(float x, float y) {
return pixelAlphaNotZero(x, y);
}
private boolean pixelAlphaNotZero(float x, float y) {
Bitmap bitmap = getBitmap();
if (bitmap == null) return false;
imageProjectionMatrix.invert(temp);
float[] onBmp = new float[2];
temp.mapPoints(onBmp, new float[]{ x, y });
int xInt = (int) onBmp[0];
int yInt = (int) onBmp[1];
if (xInt >= 0 && xInt < bitmap.getWidth() && yInt >= 0 && yInt < bitmap.getHeight()) {
return (bitmap.getPixel(xInt, yInt) & 0xff000000) != 0;
} else {
return false;
}
}
/**
* Always use this getter, as Bitmap is kept in Glide's LRUCache, so it could have been recycled
* by Glide. If it has, or was never set, this method returns null.
*/
private @Nullable Bitmap getBitmap() {
if (bitmap != null && bitmap.isRecycled()) {
bitmap = null;
}
return bitmap;
}
private void setBitmap(@NonNull RendererContext rendererContext, @Nullable Bitmap bitmap) {
this.bitmap = bitmap;
if (bitmap != null) {
RectF from = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
imageProjectionMatrix.setRectToRect(from, Bounds.FULL_BOUNDS, Matrix.ScaleToFit.CENTER);
rendererContext.rendererReady.onReady(UriGlideRenderer.this, cropMatrix(bitmap), new Point(bitmap.getWidth(), bitmap.getHeight()));
}
}
private static Matrix cropMatrix(Bitmap bitmap) {
Matrix matrix = new Matrix();
if (bitmap.getWidth() > bitmap.getHeight()) {
matrix.preScale(1, ((float) bitmap.getHeight()) / bitmap.getWidth());
} else {
matrix.preScale(((float) bitmap.getWidth()) / bitmap.getHeight(), 1);
}
return matrix;
}
public static final Creator<UriGlideRenderer> CREATOR = new Creator<UriGlideRenderer>() {
@Override
public UriGlideRenderer createFromParcel(Parcel in) {
return new UriGlideRenderer(Uri.parse(in.readString()),
in.readInt() == 1,
in.readInt(),
in.readInt()
);
}
@Override
public UriGlideRenderer[] newArray(int size) {
return new UriGlideRenderer[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(imageUri.toString());
dest.writeInt(decryptable ? 1 : 0);
dest.writeInt(maxWidth);
dest.writeInt(maxHeight);
}
}

View File

@@ -0,0 +1,73 @@
package org.thoughtcrime.securesms.scribbles.widget;
import android.graphics.PorterDuff;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import org.thoughtcrime.securesms.R;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class ColorPaletteAdapter extends RecyclerView.Adapter<ColorPaletteAdapter.ColorViewHolder> {
private final List<Integer> colors = new ArrayList<>();
private EventListener eventListener;
@Override
public @NonNull ColorViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ColorViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_color, parent, false));
}
@Override
public void onBindViewHolder(@NonNull ColorViewHolder holder, int position) {
holder.bind(colors.get(position), eventListener);
}
@Override
public int getItemCount() {
return colors.size();
}
public void setColors(@NonNull Collection<Integer> colors) {
this.colors.clear();
this.colors.addAll(colors);
notifyDataSetChanged();
}
public void setEventListener(@Nullable EventListener eventListener) {
this.eventListener = eventListener;
notifyDataSetChanged();
}
public interface EventListener {
void onColorSelected(int color);
}
static class ColorViewHolder extends RecyclerView.ViewHolder {
ImageView foreground;
ColorViewHolder(View itemView) {
super(itemView);
foreground = itemView.findViewById(R.id.palette_item_foreground);
}
void bind(int color, @Nullable EventListener eventListener) {
foreground.setColorFilter(color, PorterDuff.Mode.SRC_IN);
if (eventListener != null) {
itemView.setOnClickListener(v -> eventListener.onColorSelected(color));
}
}
}
}

View File

@@ -0,0 +1,239 @@
/**
* Copyright (c) 2016 Mark Charles
* Copyright (c) 2016 Open Whisper Systems
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.thoughtcrime.securesms.scribbles.widget;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.Shader;
import android.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import org.thoughtcrime.securesms.R;
public class VerticalSlideColorPicker extends View {
private static final float INDICATOR_TO_BAR_WIDTH_RATIO = 0.5f;
private Paint paint;
private Paint strokePaint;
private Paint indicatorStrokePaint;
private Paint indicatorFillPaint;
private Path path;
private Bitmap bitmap;
private Canvas bitmapCanvas;
private int viewWidth;
private int viewHeight;
private int centerX;
private float colorPickerRadius;
private RectF colorPickerBody;
private OnColorChangeListener onColorChangeListener;
private int borderColor;
private float borderWidth;
private float indicatorRadius;
private int[] colors;
private int touchY;
private int activeColor;
public VerticalSlideColorPicker(Context context) {
super(context);
init();
}
public VerticalSlideColorPicker(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.VerticalSlideColorPicker, 0, 0);
try {
int colorsResourceId = a.getResourceId(R.styleable.VerticalSlideColorPicker_pickerColors, R.array.scribble_colors);
colors = a.getResources().getIntArray(colorsResourceId);
borderColor = a.getColor(R.styleable.VerticalSlideColorPicker_pickerBorderColor, Color.WHITE);
borderWidth = a.getDimension(R.styleable.VerticalSlideColorPicker_pickerBorderWidth, 10f);
} finally {
a.recycle();
}
init();
}
public VerticalSlideColorPicker(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public VerticalSlideColorPicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
setWillNotDraw(false);
paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(true);
path = new Path();
strokePaint = new Paint();
strokePaint.setStyle(Paint.Style.STROKE);
strokePaint.setColor(borderColor);
strokePaint.setAntiAlias(true);
strokePaint.setStrokeWidth(borderWidth);
indicatorStrokePaint = new Paint(strokePaint);
indicatorStrokePaint.setStrokeWidth(borderWidth / 2);
indicatorFillPaint = new Paint();
indicatorFillPaint.setStyle(Paint.Style.FILL);
indicatorFillPaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
path.addCircle(centerX, borderWidth + colorPickerRadius + indicatorRadius, colorPickerRadius, Path.Direction.CW);
path.addRect(colorPickerBody, Path.Direction.CW);
path.addCircle(centerX, viewHeight - (borderWidth + colorPickerRadius + indicatorRadius), colorPickerRadius, Path.Direction.CW);
bitmapCanvas.drawColor(Color.TRANSPARENT);
bitmapCanvas.drawPath(path, strokePaint);
bitmapCanvas.drawPath(path, paint);
canvas.drawBitmap(bitmap, 0, 0, null);
touchY = Math.max((int) colorPickerBody.top, touchY);
indicatorFillPaint.setColor(activeColor);
canvas.drawCircle(centerX, touchY, indicatorRadius, indicatorFillPaint);
canvas.drawCircle(centerX, touchY, indicatorRadius, indicatorStrokePaint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
touchY = (int) Math.min(event.getY(), colorPickerBody.bottom);
touchY = (int) Math.max(colorPickerBody.top, touchY);
activeColor = bitmap.getPixel(viewWidth/2, touchY);
if (onColorChangeListener != null) {
onColorChangeListener.onColorChange(activeColor);
}
invalidate();
return true;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
viewWidth = w;
viewHeight = h;
if (viewWidth <= 0 || viewHeight <= 0) return;
int barWidth = (int) (viewWidth * INDICATOR_TO_BAR_WIDTH_RATIO);
centerX = viewWidth / 2;
indicatorRadius = (viewWidth / 2) - borderWidth;
colorPickerRadius = (barWidth / 2) - borderWidth;
colorPickerBody = new RectF(centerX - colorPickerRadius,
borderWidth + colorPickerRadius + indicatorRadius,
centerX + colorPickerRadius,
viewHeight - (borderWidth + colorPickerRadius + indicatorRadius));
LinearGradient gradient = new LinearGradient(0, colorPickerBody.top, 0, colorPickerBody.bottom, colors, null, Shader.TileMode.CLAMP);
paint.setShader(gradient);
if (bitmap != null) {
bitmap.recycle();
}
bitmap = Bitmap.createBitmap(viewWidth, viewHeight, Bitmap.Config.ARGB_8888);
bitmapCanvas = new Canvas(bitmap);
}
public void setBorderColor(int borderColor) {
this.borderColor = borderColor;
invalidate();
}
public void setBorderWidth(float borderWidth) {
this.borderWidth = borderWidth;
invalidate();
}
public void setColors(int[] colors) {
this.colors = colors;
invalidate();
}
public void setActiveColor(int color) {
activeColor = color;
if (colorPickerBody != null) {
touchY = (int) colorPickerBody.top;
}
if (onColorChangeListener != null) {
onColorChangeListener.onColorChange(color);
}
invalidate();
}
public int getActiveColor() {
return activeColor;
}
public void setOnColorChangeListener(OnColorChangeListener onColorChangeListener) {
this.onColorChangeListener = onColorChangeListener;
}
public interface OnColorChangeListener {
void onColorChange(int selectedColor);
}
}