diff --git a/image-editor/app/.gitignore b/image-editor/app/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/image-editor/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/image-editor/app/build.gradle b/image-editor/app/build.gradle
new file mode 100644
index 000000000..c54e9cd8b
--- /dev/null
+++ b/image-editor/app/build.gradle
@@ -0,0 +1,49 @@
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+ id 'kotlin-kapt'
+ id 'witness'
+}
+
+apply from: 'witness-verifications.gradle'
+
+android {
+ compileSdk COMPILE_SDK
+
+ defaultConfig {
+ applicationId "org.signal.imageeditor.app"
+ versionCode 1
+ versionName "1.0"
+
+ minSdk MINIMUM_SDK
+ targetSdk TARGET_SDK
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JAVA_VERSION
+ targetCompatibility JAVA_VERSION
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+}
+
+dependencyVerification {
+ configuration = '(debug|release)RuntimeClasspath'
+}
+
+dependencies {
+ implementation libs.androidx.core.ktx
+ implementation libs.androidx.appcompat
+ implementation libs.material.material
+ implementation project(':image-editor')
+
+ implementation libs.glide.glide
+ kapt libs.glide.compiler
+}
\ No newline at end of file
diff --git a/image-editor/app/proguard-rules.pro b/image-editor/app/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/image-editor/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/image-editor/app/src/androidTest/java/com/example/imageeditor/app/ExampleInstrumentedTest.kt b/image-editor/app/src/androidTest/java/com/example/imageeditor/app/ExampleInstrumentedTest.kt
new file mode 100644
index 000000000..f3e60adf1
--- /dev/null
+++ b/image-editor/app/src/androidTest/java/com/example/imageeditor/app/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.example.imageeditor.app
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.example.imageeditor.app", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/image-editor/app/src/main/AndroidManifest.xml b/image-editor/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..2102741cd
--- /dev/null
+++ b/image-editor/app/src/main/AndroidManifest.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/image-editor/app/src/main/java/org/signal/imageeditor/app/MainActivity.java b/image-editor/app/src/main/java/org/signal/imageeditor/app/MainActivity.java
new file mode 100644
index 000000000..cb5a2d5e7
--- /dev/null
+++ b/image-editor/app/src/main/java/org/signal/imageeditor/app/MainActivity.java
@@ -0,0 +1,275 @@
+package org.signal.imageeditor.app;
+
+import android.Manifest;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+
+import org.signal.imageeditor.app.renderers.UriRenderer;
+import org.signal.imageeditor.app.renderers.UrlRenderer;
+import org.signal.imageeditor.core.ImageEditorView;
+import org.signal.imageeditor.core.UndoRedoStackListener;
+import org.signal.imageeditor.core.model.EditorElement;
+import org.signal.imageeditor.core.model.EditorModel;
+import org.signal.imageeditor.core.renderers.MultiLineTextRenderer;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Locale;
+
+public final class MainActivity extends AppCompatActivity {
+
+ private static final String TAG = "MainActivity";
+
+ private ImageEditorView imageEditorView;
+ private Menu menu;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main_activity);
+
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ toolbar.setTitle(R.string.app_name_short);
+ setSupportActionBar(toolbar);
+
+ imageEditorView = findViewById(R.id.image_editor);
+
+ imageEditorView.setUndoRedoStackListener((undoAvailable, redoAvailable) -> {
+ Log.d("ALAN", String.format("Undo/Redo available: %s, %s", undoAvailable ? "Y" : "N", redoAvailable ? "Y" : "N"));
+ if (menu == null) return;
+ MenuItem undo = menu.findItem(R.id.action_undo);
+ MenuItem redo = menu.findItem(R.id.action_redo);
+ if (undo != null) undo.setVisible(undoAvailable);
+ if (redo != null) redo.setVisible(redoAvailable);
+ });
+
+ EditorModel model = null;
+ if (savedInstanceState != null) {
+ model = savedInstanceState.getParcelable("MODEL");
+ Log.d("ALAN", "Restoring instance " + (model != null ? model.hashCode() : 0));
+ }
+
+ if (model == null) {
+ model = initialModel();
+ Log.d("ALAN", "New instance created " + model.hashCode());
+ }
+
+ imageEditorView.setModel(model);
+
+ imageEditorView.setTapListener(new ImageEditorView.TapListener() {
+ @Override
+ public void onEntityDown(@Nullable EditorElement editorElement) {
+ Log.d("ALAN", "Entity down " + editorElement);
+ }
+
+ @Override
+ public void onEntitySingleTap(@Nullable EditorElement editorElement) {
+ Log.d("ALAN", "Entity single tapped " + editorElement);
+ }
+
+ @Override
+ public void onEntityDoubleTap(@NonNull EditorElement editorElement) {
+ Log.d("ALAN", "Entity double tapped " + editorElement);
+ if (editorElement.getRenderer() instanceof MultiLineTextRenderer) {
+ imageEditorView.startTextEditing(editorElement);
+ } else {
+ imageEditorView.deleteElement(editorElement);
+ }
+ }
+ });
+ }
+
+ private static EditorModel initialModel() {
+
+ EditorModel model = EditorModel.create();
+
+ EditorElement image = new EditorElement(new UrlRenderer("https://cdn.aarp.net/content/dam/aarp/home-and-family/your-home/2018/06/1140-house-inheriting.imgcache.rev68c065601779c5d76b913cf9ec3a977e.jpg"));
+ image.getFlags().setSelectable(false).persist();
+ model.addElement(image);
+
+ EditorElement elementC = new EditorElement(new UrlRenderer("https://upload.wikimedia.org/wikipedia/commons/thumb/e/e0/SNice.svg/220px-SNice.svg.png"));
+ elementC.getLocalMatrix().postScale(0.2f, 0.2f);
+ //elementC.getLocalMatrix().postRotate(30);
+ model.addElement(elementC);
+
+ EditorElement elementE = new EditorElement(new UrlRenderer("https://www.vitalessentialsraw.com/assets/images/background-images/laying-grey-cat.png"));
+ elementE.getLocalMatrix().postScale(0.2f, 0.2f);
+ //elementE.getLocalMatrix().postRotate(60);
+ model.addElement(elementE);
+
+ EditorElement elementD = new EditorElement(new UrlRenderer("https://petspluslubbocktx.com/files/2016/11/DC-Cat-Weight-Management.png"));
+ elementD.getLocalMatrix().postScale(0.2f, 0.2f);
+ //elementD.getLocalMatrix().postRotate(60);
+ model.addElement(elementD);
+
+ EditorElement elementF = new EditorElement(new UrlRenderer("https://purepng.com/public/uploads/large/purepng.com-black-top-hathatsstandard-sizeblacktop-14215263591972x0zh.png"));
+ elementF.getLocalMatrix().postScale(0.2f, 0.2f);
+ //elementF.getLocalMatrix().postRotatF(60);
+ model.addElement(elementF);
+
+ EditorElement elementG = new EditorElement(new UriRenderer(Uri.parse("file:///android_asset/food/apple.png")));
+ elementG.getLocalMatrix().postScale(0.2f, 0.2f);
+ //elementG.getLocalMatrix().postRotatG(60);
+ model.addElement(elementG);
+
+ EditorElement elementH = new EditorElement(new MultiLineTextRenderer("Hello, World!", 0xff0000ff, MultiLineTextRenderer.Mode.REGULAR));
+ //elementH.getLocalMatrix().postScale(0.2f, 0.2f);
+ model.addElement(elementH);
+
+ EditorElement elementH2 = new EditorElement(new MultiLineTextRenderer("Hello, World 2!", 0xff0000ff, MultiLineTextRenderer.Mode.REGULAR));
+ //elementH.getLocalMatrix().postScale(0.2f, 0.2f);
+ model.addElement(elementH2);
+
+ return model;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putParcelable("MODEL", imageEditorView.getModel());
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.action_menu, menu);
+ this.menu = menu;
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_undo:
+ imageEditorView.getModel().undo();
+ Log.d(TAG, String.format("Model is %s", imageEditorView.getModel().isChanged() ? "changed" : "unchanged"));
+ return true;
+
+ case R.id.action_redo:
+ imageEditorView.getModel().redo();
+ return true;
+
+ case R.id.action_crop:
+ imageEditorView.setMode(ImageEditorView.Mode.MoveAndResize);
+ imageEditorView.getModel().startCrop();
+ return true;
+
+ case R.id.action_done:
+ imageEditorView.setMode(ImageEditorView.Mode.MoveAndResize);
+ imageEditorView.getModel().doneCrop();
+ return true;
+
+ case R.id.action_draw:
+ imageEditorView.setDrawingBrushColor(0xffffff00);
+ imageEditorView.startDrawing(0.02f, Paint.Cap.ROUND, false);
+ return true;
+
+ case R.id.action_rotate_right_90:
+ imageEditorView.getModel().rotate90clockwise();
+ return true;
+
+ case R.id.action_rotate_left_90:
+ imageEditorView.getModel().rotate90anticlockwise();
+ return true;
+
+ case R.id.action_flip_horizontal:
+ imageEditorView.getModel().flipHorizontal();
+ return true;
+
+ case R.id.action_flip_vertical:
+ imageEditorView.getModel().flipVertical();
+ return true;
+
+ case R.id.action_edit_text:
+ editText();
+ return true;
+
+ case R.id.action_lock_crop_aspect:
+ imageEditorView.getModel().setCropAspectLock(!imageEditorView.getModel().isCropAspectLocked());
+ return true;
+
+ case R.id.action_save:
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED) {
+ ActivityCompat.requestPermissions(this,
+ new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE },
+ 0);
+ } else {
+ Bitmap bitmap = imageEditorView.getModel().render(this);
+ try {
+ Uri uri = saveBmp(bitmap);
+
+ Intent intent = new Intent();
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.setDataAndType(uri, "image/*");
+ startActivity(intent);
+
+ } finally {
+ bitmap.recycle();
+ }
+ }
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ private void editText() {
+ imageEditorView.getModel().getRoot().findElement(new Matrix(), new Matrix(), (element, inverseMatrix) -> {
+ if (element.getRenderer() instanceof MultiLineTextRenderer) {
+ imageEditorView.startTextEditing(element);
+ return true;
+ }
+ return false;
+ }
+ );
+ }
+
+ private Uri saveBmp(Bitmap bitmap) {
+ String path = Environment.getExternalStorageDirectory().toString();
+
+ File filePath = new File(path);
+ File imageEditor = new File(filePath, "ImageEditor");
+ if (!imageEditor.exists()) {
+ imageEditor.mkdir();
+ }
+
+ int counter = 0;
+ File file;
+ do {
+ counter++;
+ file = new File(imageEditor, String.format(Locale.US, "ImageEditor_%03d.jpg", counter));
+ } while (file.exists());
+
+ try {
+ try (OutputStream stream = new FileOutputStream(file)) {
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
+ }
+ return Uri.parse(MediaStore.Images.Media.insertImage(getContentResolver(), file.getAbsolutePath(), file.getName(), file.getName()));
+ } catch (FileNotFoundException e) {
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/image-editor/app/src/main/java/org/signal/imageeditor/app/TheAppGlideModule.java b/image-editor/app/src/main/java/org/signal/imageeditor/app/TheAppGlideModule.java
new file mode 100644
index 000000000..934542c77
--- /dev/null
+++ b/image-editor/app/src/main/java/org/signal/imageeditor/app/TheAppGlideModule.java
@@ -0,0 +1,10 @@
+package org.signal.imageeditor.app;
+
+import com.bumptech.glide.annotation.GlideModule;
+import com.bumptech.glide.module.AppGlideModule;
+
+@GlideModule
+public class TheAppGlideModule extends AppGlideModule {
+
+
+}
diff --git a/image-editor/app/src/main/java/org/signal/imageeditor/app/renderers/StandardHitTestRenderer.java b/image-editor/app/src/main/java/org/signal/imageeditor/app/renderers/StandardHitTestRenderer.java
new file mode 100644
index 000000000..008f9db21
--- /dev/null
+++ b/image-editor/app/src/main/java/org/signal/imageeditor/app/renderers/StandardHitTestRenderer.java
@@ -0,0 +1,12 @@
+package org.signal.imageeditor.app.renderers;
+
+import org.signal.imageeditor.core.Bounds;
+import org.signal.imageeditor.core.Renderer;
+
+public abstract class StandardHitTestRenderer implements Renderer {
+
+ @Override
+ public boolean hitTest(float x, float y) {
+ return Bounds.contains(x, y);
+ }
+}
diff --git a/image-editor/app/src/main/java/org/signal/imageeditor/app/renderers/UriRenderer.java b/image-editor/app/src/main/java/org/signal/imageeditor/app/renderers/UriRenderer.java
new file mode 100644
index 000000000..3f5fa6f1f
--- /dev/null
+++ b/image-editor/app/src/main/java/org/signal/imageeditor/app/renderers/UriRenderer.java
@@ -0,0 +1,138 @@
+package org.signal.imageeditor.app.renderers;
+
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import com.bumptech.glide.request.target.SimpleTarget;
+import com.bumptech.glide.request.transition.Transition;
+
+import org.signal.imageeditor.app.GlideApp;
+import org.signal.imageeditor.core.Bounds;
+import org.signal.imageeditor.core.Renderer;
+import org.signal.imageeditor.core.RendererContext;
+
+public final class UriRenderer implements Renderer, Parcelable {
+
+ private final Uri imageUri;
+
+ private final Paint paint = new Paint();
+
+ private final Matrix temp1 = new Matrix();
+ private final Matrix temp2 = new Matrix();
+
+ @Nullable
+ private Bitmap bitmap;
+
+ public UriRenderer(Uri imageUri) {
+ this.imageUri = imageUri;
+ paint.setAntiAlias(true);
+ }
+
+ private UriRenderer(Parcel in) {
+ this(Uri.parse(in.readString()));
+ }
+
+ @Override
+ public void render(@NonNull RendererContext rendererContext) {
+ if (bitmap != null && bitmap.isRecycled()) bitmap = null;
+
+ if (bitmap == null) {
+ GlideApp.with(rendererContext.context)
+ .asBitmap()
+ .load(imageUri)
+ .diskCacheStrategy(DiskCacheStrategy.ALL)
+ .into(new SimpleTarget() {
+ @Override
+ public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition super Bitmap> transition) {
+ setBitmap(resource);
+ rendererContext.rendererReady.onReady(UriRenderer.this, cropMatrix(resource), new Point(resource.getWidth(), resource.getHeight()));
+ }
+ });
+ }
+
+ if (bitmap != null) {
+ rendererContext.save();
+ rendererContext.canvasMatrix.concat(temp1);
+
+ // FYI units are pixels at this point.
+ paint.setAlpha(rendererContext.getAlpha(255));
+ rendererContext.canvas.drawBitmap(bitmap, 0, 0, paint);
+ rendererContext.restore();
+ } else {
+ rendererContext.canvas.drawRect(-0.5f, -0.5f, 0.5f, 0.5f, paint);
+ }
+ }
+
+ @Override
+ public boolean hitTest(float x, float y) {
+ return pixelNotAlpha(x, y);
+ }
+
+ private boolean pixelNotAlpha(float x, float y) {
+ if (bitmap == null) return false;
+
+ temp1.invert(temp2);
+
+ float[] onBmp = new float[2];
+ temp2.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;
+ }
+ }
+
+ private void setBitmap(Bitmap bitmap) {
+ if (bitmap != null) {
+ this.bitmap = bitmap;
+ RectF from = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
+ temp1.setRectToRect(from, Bounds.FULL_BOUNDS, Matrix.ScaleToFit.CENTER);
+ }
+ }
+
+ 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 CREATOR = new Creator() {
+ @Override
+ public UriRenderer createFromParcel(Parcel in) {
+ return new UriRenderer(in);
+ }
+
+ @Override
+ public UriRenderer[] newArray(int size) {
+ return new UriRenderer[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(imageUri.toString());
+ }
+}
diff --git a/image-editor/app/src/main/java/org/signal/imageeditor/app/renderers/UrlRenderer.java b/image-editor/app/src/main/java/org/signal/imageeditor/app/renderers/UrlRenderer.java
new file mode 100644
index 000000000..01d5f9ce5
--- /dev/null
+++ b/image-editor/app/src/main/java/org/signal/imageeditor/app/renderers/UrlRenderer.java
@@ -0,0 +1,263 @@
+package org.signal.imageeditor.app.renderers;
+
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.RectF;
+import android.os.Parcel;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import com.bumptech.glide.request.target.SimpleTarget;
+import com.bumptech.glide.request.transition.Transition;
+
+import org.signal.imageeditor.app.GlideApp;
+import org.signal.imageeditor.core.Bounds;
+import org.signal.imageeditor.core.RendererContext;
+
+import java.util.concurrent.ExecutionException;
+
+public final class UrlRenderer extends StandardHitTestRenderer {
+
+ private static final String TAG = "UrlRenderer";
+ private final String url;
+ private final Paint paint = new Paint();
+
+ private final Matrix temp1 = new Matrix();
+ private final Matrix temp2 = new Matrix();
+
+ private Bitmap bitmap;
+
+ public UrlRenderer(@Nullable String url) {
+ this.url = url;
+ paint.setAntiAlias(true);
+ }
+
+ private UrlRenderer(Parcel in) {
+ this(in.readString());
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public UrlRenderer createFromParcel(Parcel in) {
+ return new UrlRenderer(in);
+ }
+
+ @Override
+ public UrlRenderer[] newArray(int size) {
+ return new UrlRenderer[size];
+ }
+ };
+
+ @Override
+ public void render(@NonNull RendererContext rendererContext) {
+ if (bitmap != null && bitmap.isRecycled()) bitmap = null;
+
+ if (bitmap == null) {
+ if (rendererContext.isBlockingLoad()) {
+ try {
+ setBitmap(rendererContext, GlideApp.with(rendererContext.context)
+ .asBitmap()
+ .load(url)
+ .diskCacheStrategy(DiskCacheStrategy.ALL)
+ .submit()
+ .get());
+ } catch (ExecutionException e) {
+ throw new RuntimeException(e);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ } else {
+ GlideApp.with(rendererContext.context)
+ .asBitmap()
+ .load(url)
+ .diskCacheStrategy(DiskCacheStrategy.NONE)
+ .into(new SimpleTarget() {
+ @Override
+ public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition super Bitmap> transition) {
+ setBitmap(rendererContext, resource);
+ }
+ });
+ }
+ }
+
+ if (bitmap != null) {
+ rendererContext.save();
+ rendererContext.getCurrent(temp2);
+ temp2.preConcat(temp1);
+ rendererContext.canvas.concat(temp1);
+
+ // FYI units are pixels at this point.
+ paint.setAlpha(rendererContext.getAlpha(255));
+ rendererContext.canvas.drawBitmap(bitmap, 0, 0, paint);
+ rendererContext.restore();
+ } else {
+ if (rendererContext.isBlockingLoad()) {
+ Log.e(TAG, "blocking but drawing null :(");
+ }
+ rendererContext.canvas.drawRect(Bounds.FULL_BOUNDS, paint);
+ }
+
+ drawDebugInfo(rendererContext);
+ }
+
+ private void drawDebugInfo(RendererContext rendererContext) {
+// float width = bitmap.getWidth();
+// float height = bitmap.getWidth();
+
+ //RectF bounds = new RectF(Bounds.LEFT, Bounds.TOP/2f, Bounds.RIGHT,Bounds.BOTTOM/2f );//Bounds.FULL_BOUNDS;
+ RectF bounds = Bounds.FULL_BOUNDS;
+
+ Paint paint = new Paint();
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setColor(0xffffff00);
+ rendererContext.canvas.drawRect(bounds, paint);
+
+ RectF fullBounds = new RectF();
+ rendererContext.mapRect(fullBounds, bounds);
+
+ rendererContext.save();
+
+ RectF dst = new RectF();
+ rendererContext.mapRect(dst, bounds);
+ paint.setColor(0xffff00ff);
+ rendererContext.canvasMatrix.setToIdentity();
+ rendererContext.canvas.drawRect(dst, paint);
+
+ rendererContext.restore();
+
+ rendererContext.save();
+
+ Matrix unrotated = new Matrix();
+ rendererContext.getCurrent(unrotated);
+ findUnrotateMatrix(unrotated);
+
+ Matrix rotated = new Matrix();
+ rendererContext.getCurrent(rotated);
+ findRotateMatrix(rotated);
+
+ RectF dst2 = new RectF();
+ unrotated.mapRect(dst2, Bounds.FULL_BOUNDS); // works because square, do we need rotated here?
+
+ float scaleX = Bounds.FULL_BOUNDS.width() / dst2.width();
+ float scaleY = Bounds.FULL_BOUNDS.height() / dst2.height();
+
+ rendererContext.canvasMatrix.concat(unrotated);
+ Matrix matrix = new Matrix();
+ matrix.setScale(scaleX, scaleY);
+ rendererContext.canvasMatrix.concat(matrix);
+
+ paint.setColor(0xff0000ff);
+ rendererContext.canvas.drawRect(bounds, paint);
+
+ rendererContext.restore();
+ }
+
+/**
+ * Given a scaled/rotated and transformed matrix, extract just the rotate and reverse it.
+ */
+ private void findUnrotateMatrix(@NonNull Matrix matrix) {
+ float[] values = new float[9];
+
+ matrix.getValues(values);
+
+ float xScale = (float) Math.sqrt(values[0] * values[0] + values[3] * values[3]);
+ float yScale = (float) Math.sqrt(values[1] * values[1] + values[4] * values[4]);
+
+ values[0] /= xScale;
+ values[1] /= -yScale;
+ values[2] = 0;
+
+ values[3] /= -xScale;
+ values[4] /= yScale;
+ values[5] = 0;
+
+ matrix.setValues(values);
+ }
+
+ /**
+ * Given a scaled/rotated and transformed matrix, extract just the rotate and reverse it.
+ */
+ private void findRotateMatrix(@NonNull Matrix matrix) {
+ float[] values = new float[9];
+
+ matrix.getValues(values);
+
+ float xScale = (float) Math.sqrt(values[0] * values[0] + values[3] * values[3]);
+ float yScale = (float) Math.sqrt(values[1] * values[1] + values[4] * values[4]);
+
+ values[0] /= xScale;
+ values[1] /= yScale;
+ values[2] = 0;
+
+ values[3] /= xScale;
+ values[4] /= yScale;
+ values[5] = 0;
+
+ matrix.setValues(values);
+ }
+
+ @Override
+ public boolean hitTest(float x, float y) {
+ return super.hitTest(x, y) && pixelNotAlpha(x, y);
+ }
+
+ private boolean pixelNotAlpha(float x, float y) {
+ if (bitmap == null) return false;
+
+ temp1.invert(temp2);
+
+ float[] onBmp = new float[2];
+ temp2.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 xInt >= 0 && xInt <= bitmap.getWidth() && yInt >= 0 && yInt <= bitmap.getHeight();
+ }
+ }
+
+ 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());
+ temp1.setRectToRect(from, Bounds.FULL_BOUNDS, Matrix.ScaleToFit.CENTER);
+ rendererContext.rendererReady.onReady(this, cropMatrix(bitmap), new Point(bitmap.getWidth(), bitmap.getHeight()));
+ }
+ }
+
+ private void setBitmap(Bitmap bitmap) {
+ if (bitmap != null) {
+ this.bitmap = bitmap;
+ RectF from = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
+ temp1.setRectToRect(from, Bounds.FULL_BOUNDS, Matrix.ScaleToFit.CENTER);
+ }
+ }
+
+ 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;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(url);
+ }
+}
diff --git a/image-editor/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/image-editor/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 000000000..2b068d114
--- /dev/null
+++ b/image-editor/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/image-editor/app/src/main/res/drawable/ic_check_black_24dp.xml b/image-editor/app/src/main/res/drawable/ic_check_black_24dp.xml
new file mode 100644
index 000000000..3c728c59f
--- /dev/null
+++ b/image-editor/app/src/main/res/drawable/ic_check_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/image-editor/app/src/main/res/drawable/ic_crop_black_24dp.xml b/image-editor/app/src/main/res/drawable/ic_crop_black_24dp.xml
new file mode 100644
index 000000000..5a4749cdb
--- /dev/null
+++ b/image-editor/app/src/main/res/drawable/ic_crop_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/image-editor/app/src/main/res/drawable/ic_flip_black_24dp.xml b/image-editor/app/src/main/res/drawable/ic_flip_black_24dp.xml
new file mode 100644
index 000000000..2bc2762d4
--- /dev/null
+++ b/image-editor/app/src/main/res/drawable/ic_flip_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/image-editor/app/src/main/res/drawable/ic_launcher_background.xml b/image-editor/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..07d5da9cb
--- /dev/null
+++ b/image-editor/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/image-editor/app/src/main/res/drawable/ic_redo_black_24dp.xml b/image-editor/app/src/main/res/drawable/ic_redo_black_24dp.xml
new file mode 100644
index 000000000..424e788f5
--- /dev/null
+++ b/image-editor/app/src/main/res/drawable/ic_redo_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/image-editor/app/src/main/res/drawable/ic_rotate_left_black_24dp.xml b/image-editor/app/src/main/res/drawable/ic_rotate_left_black_24dp.xml
new file mode 100644
index 000000000..2fd476dcd
--- /dev/null
+++ b/image-editor/app/src/main/res/drawable/ic_rotate_left_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/image-editor/app/src/main/res/drawable/ic_rotate_right_black_24dp.xml b/image-editor/app/src/main/res/drawable/ic_rotate_right_black_24dp.xml
new file mode 100644
index 000000000..a98657481
--- /dev/null
+++ b/image-editor/app/src/main/res/drawable/ic_rotate_right_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/image-editor/app/src/main/res/drawable/ic_save_black_24dp.xml b/image-editor/app/src/main/res/drawable/ic_save_black_24dp.xml
new file mode 100644
index 000000000..a561d632a
--- /dev/null
+++ b/image-editor/app/src/main/res/drawable/ic_save_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/image-editor/app/src/main/res/drawable/ic_undo_black_24dp.xml b/image-editor/app/src/main/res/drawable/ic_undo_black_24dp.xml
new file mode 100644
index 000000000..5558e37d7
--- /dev/null
+++ b/image-editor/app/src/main/res/drawable/ic_undo_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/image-editor/app/src/main/res/layout/main_activity.xml b/image-editor/app/src/main/res/layout/main_activity.xml
new file mode 100644
index 000000000..d2d1baf86
--- /dev/null
+++ b/image-editor/app/src/main/res/layout/main_activity.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/image-editor/app/src/main/res/menu/action_menu.xml b/image-editor/app/src/main/res/menu/action_menu.xml
new file mode 100644
index 000000000..ac001f4d0
--- /dev/null
+++ b/image-editor/app/src/main/res/menu/action_menu.xml
@@ -0,0 +1,73 @@
+
+
\ No newline at end of file
diff --git a/image-editor/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/image-editor/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..eca70cfe5
--- /dev/null
+++ b/image-editor/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/image-editor/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/image-editor/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..eca70cfe5
--- /dev/null
+++ b/image-editor/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/image-editor/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/image-editor/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 000000000..c209e78ec
Binary files /dev/null and b/image-editor/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/image-editor/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/image-editor/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..b2dfe3d1b
Binary files /dev/null and b/image-editor/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/image-editor/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/image-editor/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 000000000..4f0f1d64e
Binary files /dev/null and b/image-editor/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/image-editor/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/image-editor/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..62b611da0
Binary files /dev/null and b/image-editor/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/image-editor/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/image-editor/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 000000000..948a3070f
Binary files /dev/null and b/image-editor/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/image-editor/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/image-editor/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..1b9a6956b
Binary files /dev/null and b/image-editor/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/image-editor/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/image-editor/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 000000000..28d4b77f9
Binary files /dev/null and b/image-editor/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/image-editor/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/image-editor/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..9287f5083
Binary files /dev/null and b/image-editor/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/image-editor/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/image-editor/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 000000000..aa7d6427e
Binary files /dev/null and b/image-editor/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/image-editor/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/image-editor/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..9126ae37c
Binary files /dev/null and b/image-editor/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/image-editor/app/src/main/res/values/colors.xml b/image-editor/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..d2cd14a9f
--- /dev/null
+++ b/image-editor/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #008577
+ #00574B
+ #D81B60
+
\ No newline at end of file
diff --git a/image-editor/app/src/main/res/values/strings.xml b/image-editor/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..e4504e48b
--- /dev/null
+++ b/image-editor/app/src/main/res/values/strings.xml
@@ -0,0 +1,17 @@
+
+ Image Editor Sample App
+ Image Editor
+
+ Undo
+ Redo
+ Crop
+ Done
+ Save
+ Draw
+ Rotate 90 right
+ Rotate 90 left
+ Flip horizontal
+ Flip vertical
+ Edit Text
+ Lock crop aspect
+
\ No newline at end of file
diff --git a/image-editor/app/src/main/res/values/themes.xml b/image-editor/app/src/main/res/values/themes.xml
new file mode 100644
index 000000000..ab7e2b3f8
--- /dev/null
+++ b/image-editor/app/src/main/res/values/themes.xml
@@ -0,0 +1,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/image-editor/app/src/test/java/com/example/imageeditor/app/ExampleUnitTest.kt b/image-editor/app/src/test/java/com/example/imageeditor/app/ExampleUnitTest.kt
new file mode 100644
index 000000000..03c24560b
--- /dev/null
+++ b/image-editor/app/src/test/java/com/example/imageeditor/app/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.example.imageeditor.app
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/image-editor/app/witness-verifications.gradle b/image-editor/app/witness-verifications.gradle
new file mode 100644
index 000000000..77afd866c
--- /dev/null
+++ b/image-editor/app/witness-verifications.gradle
@@ -0,0 +1,150 @@
+// Auto-generated, use ./gradlew calculateChecksums to regenerate
+
+dependencyVerification {
+ verify = [
+
+ ['androidx.activity:activity:1.0.0',
+ 'd1bc9842455c2e534415d88c44df4d52413b478db9093a1ba36324f705f44c3d'],
+
+ ['androidx.annotation:annotation-experimental:1.0.0',
+ 'b219d2b568e7e4ba534e09f8c2fd242343df6ccbdfbbe938846f5d740e6b0b11'],
+
+ ['androidx.annotation:annotation:1.2.0',
+ '9029262bddce116e6d02be499e4afdba21f24c239087b76b3b57d7e98b490a36'],
+
+ ['androidx.appcompat:appcompat-resources:1.2.0',
+ 'c470297c03ff3de1c3d15dacf0be0cae63abc10b52f021dd07ae28daa3100fe5'],
+
+ ['androidx.appcompat:appcompat:1.2.0',
+ '3d2131a55a61a777322e2126e0018011efa6339e53b44153eb651b16020cca70'],
+
+ ['androidx.arch.core:core-common:2.1.0',
+ 'fe1237bf029d063e7f29fe39aeaf73ef74c8b0a3658486fc29d3c54326653889'],
+
+ ['androidx.arch.core:core-runtime:2.0.0',
+ '87e65fc767c712b437649c7cee2431ebb4bed6daef82e501d4125b3ed3f65f8e'],
+
+ ['androidx.cardview:cardview:1.0.0',
+ '1193c04c22a3d6b5946dae9f4e8c59d6adde6a71b6bd5d87fb99d82dda1afec7'],
+
+ ['androidx.collection:collection:1.1.0',
+ '632a0e5407461de774409352940e292a291037724207a787820c77daf7d33b72'],
+
+ ['androidx.constraintlayout:constraintlayout-solver:2.0.1',
+ 'b23732edbb3511d937fea1ffef047b0e6c001b50c1921f0d959fc384d706ec6a'],
+
+ ['androidx.constraintlayout:constraintlayout:2.0.1',
+ 'ec15b5d4a2eff07888bc1499ce2e2c6efe24c0ed60cc57b08c9dc4b6fd3c2189'],
+
+ ['androidx.coordinatorlayout:coordinatorlayout:1.1.0',
+ '44a9e30abf56af1025c52a0af506fee9c4131aa55efda52f9fd9451211c5e8cb'],
+
+ ['androidx.core:core-ktx:1.5.0',
+ '5964cfe7a4882da2a00fb6ca3d3a072d04139208186f7bc4b3cb66022764fc42'],
+
+ ['androidx.core:core:1.5.0',
+ '2b279712795689069cfb63e48b3ab63c32a5649bdda44c482eb8f81ca1a72161'],
+
+ ['androidx.cursoradapter:cursoradapter:1.0.0',
+ 'a81c8fe78815fa47df5b749deb52727ad11f9397da58b16017f4eb2c11e28564'],
+
+ ['androidx.customview:customview:1.0.0',
+ '20e5b8f6526a34595a604f56718da81167c0b40a7a94a57daa355663f2594df2'],
+
+ ['androidx.documentfile:documentfile:1.0.0',
+ '865a061ef2fad16522f8433536b8d47208c46ff7c7745197dfa1eeb481869487'],
+
+ ['androidx.drawerlayout:drawerlayout:1.0.0',
+ '9402442cdc5a43cf62fb14f8cf98c63342d4d9d9b805c8033c6cf7e802749ac1'],
+
+ ['androidx.dynamicanimation:dynamicanimation:1.0.0',
+ 'ce005162c229bf308d2d5b12fb6cad0874069cbbeaccee63a8193bd08d40de04'],
+
+ ['androidx.exifinterface:exifinterface:1.0.0',
+ 'ee48be10aab8f54efff4c14b77d11e10b9eeee4379d5ef6bf297a2923c55cc11'],
+
+ ['androidx.fragment:fragment:1.1.0',
+ 'a14c8b8f2153f128e800fbd266a6beab1c283982a29ec570d2cc05d307d81496'],
+
+ ['androidx.interpolator:interpolator:1.0.0',
+ '33193135a64fe21fa2c35eec6688f1a76e512606c0fc83dc1b689e37add7732a'],
+
+ ['androidx.legacy:legacy-support-core-utils:1.0.0',
+ 'a7edcf01d5b52b3034073027bc4775b78a4764bb6202bb91d61c829add8dd1c7'],
+
+ ['androidx.lifecycle:lifecycle-common:2.1.0',
+ '76db6be533bd730fb361c2feb12a2c26d9952824746847da82601ef81f082643'],
+
+ ['androidx.lifecycle:lifecycle-livedata-core:2.0.0',
+ 'fde334ec7e22744c0f5bfe7caf1a84c9d717327044400577bdf9bd921ec4f7bc'],
+
+ ['androidx.lifecycle:lifecycle-livedata:2.0.0',
+ 'c82609ced8c498f0a701a30fb6771bb7480860daee84d82e0a81ee86edf7ba39'],
+
+ ['androidx.lifecycle:lifecycle-runtime:2.1.0',
+ 'e5173897b965e870651e83d9d5af1742d3f532d58863223a390ce3a194c8312b'],
+
+ ['androidx.lifecycle:lifecycle-viewmodel:2.1.0',
+ 'ba55fb7ac1b2828d5327cda8acf7085d990b2b4c43ef336caa67686249b8523d'],
+
+ ['androidx.loader:loader:1.0.0',
+ '11f735cb3b55c458d470bed9e25254375b518b4b1bad6926783a7026db0f5025'],
+
+ ['androidx.localbroadcastmanager:localbroadcastmanager:1.0.0',
+ 'e71c328ceef5c4a7d76f2d86df1b65d65fe2acf868b1a4efd84a3f34336186d8'],
+
+ ['androidx.print:print:1.0.0',
+ '1d5c7f3135a1bba661fc373fd72e11eb0a4adbb3396787826dd8e4190d5d9edd'],
+
+ ['androidx.recyclerview:recyclerview:1.1.0',
+ 'f0d2b5a67d0a91ee1b1c73ef2b636a81f3563925ddd15a1d4e1c41ec28de7a4f'],
+
+ ['androidx.savedstate:savedstate:1.0.0',
+ '2510a5619c37579c9ce1a04574faaf323cd0ffe2fc4e20fa8f8f01e5bb402e83'],
+
+ ['androidx.transition:transition:1.2.0',
+ 'a1e059b3bc0b43a58dec0efecdcaa89c82d2bca552ea5bacf6656c46e853157e'],
+
+ ['androidx.vectordrawable:vectordrawable-animated:1.1.0',
+ '76da2c502371d9c38054df5e2b248d00da87809ed058f3363eae87ce5e2403f8'],
+
+ ['androidx.vectordrawable:vectordrawable:1.1.0',
+ '46fd633ac01b49b7fcabc263bf098c5a8b9e9a69774d234edcca04fb02df8e26'],
+
+ ['androidx.versionedparcelable:versionedparcelable:1.1.1',
+ '57e8d93260d18d5b9007c9eed3c64ad159de90c8609ebfc74a347cbd514535a4'],
+
+ ['androidx.viewpager2:viewpager2:1.0.0',
+ 'e95c0031d4cc247cd48196c6287e58d2cee54d9c79b85afea7c90920330275af'],
+
+ ['androidx.viewpager:viewpager:1.0.0',
+ '147af4e14a1984010d8f155e5e19d781f03c1d70dfed02a8e0d18428b8fc8682'],
+
+ ['com.github.bumptech.glide:annotations:4.11.0',
+ 'd219d238006d824962176229d4708abcdddcfe342c6a18a5d0fa48d6f0479b3e'],
+
+ ['com.github.bumptech.glide:disklrucache:4.11.0',
+ 'd06775a5171b777aa3db031eb0dd4a1dbe3f00dda35b5574dfd953f6b0d5ef18'],
+
+ ['com.github.bumptech.glide:gifdecoder:4.11.0',
+ '197a1cd5b76855aa02b230c13974e293229b901dc2b96fab4315201e78baa804'],
+
+ ['com.github.bumptech.glide:glide:4.11.0',
+ '5c294e6a5f0f812cef876b8412954c1822da184af38e082a5b766e3c4f4fcd95'],
+
+ ['com.google.android.material:material:1.3.0',
+ 'cbf1e7d69fc236cdadcbd1ec5f6c0a1a41aca6ad1ef7f8481058956270ab1f0a'],
+
+ ['com.google.protobuf:protobuf-javalite:3.10.0',
+ '215a94dbe100130295906b531bb72a26965c7ac8fcd9a75bf8054a8ac2abf4b4'],
+
+ ['org.jetbrains.kotlin:kotlin-stdlib-common:1.4.32',
+ 'e1ff6f55ee9e7591dcc633f7757bac25a7edb1cc7f738b37ec652f10f66a4145'],
+
+ ['org.jetbrains.kotlin:kotlin-stdlib:1.4.32',
+ '13e9fd3e69dc7230ce0fc873a92a4e5d521d179bcf1bef75a6705baac3bfecba'],
+
+ ['org.jetbrains:annotations:13.0',
+ 'ace2a10dc8e2d5fd34925ecac03e4988b2c0f851650c94b8cef49ba1bd111478'],
+ ]
+}
diff --git a/settings.gradle b/settings.gradle
index a86798ccb..a707d9349 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -10,6 +10,7 @@ include ':video'
include ':device-transfer'
include ':device-transfer-app'
include ':image-editor'
+include ':image-editor-app'
project(':app').name = 'Signal-Android'
project(':paging').projectDir = file('paging/lib')
@@ -21,7 +22,8 @@ project(':device-transfer-app').projectDir = file('device-transfer/app')
project(':libsignal-service').projectDir = file('libsignal/service')
project(':image-editor').projectDir = file('image-editor/lib')
+project(':image-editor-app').projectDir = file('image-editor/app')
rootProject.name='Signal'
-apply from: 'dependencies.gradle'
\ No newline at end of file
+apply from: 'dependencies.gradle'