package org.thoughtcrime.securesms.scribbles; import android.content.Intent; import android.graphics.Paint; import android.net.Uri; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; 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.scribbles.widget.VerticalSlideColorPicker; import org.thoughtcrime.securesms.util.ParcelUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; 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"; public static final int SELECT_STICKER_REQUEST_CODE = 123; private EditorModel restoredModel; @Nullable private EditorElement currentSelection; private int imageMaxHeight; private int imageMaxWidth; 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()); } @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; } else if (savedInstanceState != null) { editorModel = new Data(savedInstanceState).readModel(); } 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 onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); new Data(outState).writeModel(imageEditorView.getModel()); } @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()); } } 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_STICKER_REQUEST_CODE && data != null) { final String stickerFile = data.getStringExtra(StickerSelectActivity.EXTRA_STICKER_FILE); UriGlideRenderer renderer = new UriGlideRenderer(Uri.parse("file:///android_asset/" + stickerFile), false, imageMaxWidth, imageMaxHeight); EditorElement element = new EditorElement(renderer); imageEditorView.getModel().addElementCentered(element, 0.2f); currentSelection = element; } else { imageEditorHud.enterMode(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 MOVE_DELETE: Intent intent = new Intent(getContext(), StickerSelectActivity.class); startActivityForResult(intent, SELECT_STICKER_REQUEST_CODE); 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 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) { controller.onRequestFullScreen(fullScreen); } 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.enterMode(ImageEditorHud.Mode.NONE); imageEditorView.doneTextEditing(); } } @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.enterMode(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); } }