From c8dd4e5254c0771b2a1925df408ed9d360c68f84 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Tue, 2 Jun 2020 20:35:44 -0400 Subject: [PATCH] Added support for blurring faces. Co-authored-by: Alan Evans --- app/build.gradle | 5 +- .../securesms/gcm/FcmReceiveService.java | 4 +- .../imageeditor/ImageEditorView.java | 13 ++- .../imageeditor/model/EditorModel.java | 38 +++++++ .../renderers/FaceBlurRenderer.java | 86 ++++++++++++++ .../securesms/keyvalue/SignalStore.java | 14 ++- .../securesms/keyvalue/TooltipValues.java | 36 ++++++ .../securesms/scribbles/FaceDetector.java | 10 ++ .../scribbles/FirebaseFaceDetector.java | 79 +++++++++++++ .../scribbles/ImageEditorFragment.java | 107 ++++++++++++++++-- .../securesms/scribbles/ImageEditorHud.java | 72 +++++++++--- .../securesms/scribbles/UriGlideRenderer.java | 60 ++++++++-- .../drawable-hdpi/ic_image_editor_blur.png | Bin 0 -> 1800 bytes .../drawable-mdpi/ic_image_editor_blur.png | Bin 0 -> 1096 bytes .../drawable-xhdpi/ic_image_editor_blur.png | Bin 0 -> 2655 bytes .../drawable-xxhdpi/ic_image_editor_blur.png | Bin 0 -> 4375 bytes .../drawable-xxxhdpi/ic_image_editor_blur.png | Bin 0 -> 6738 bytes .../main/res/drawable-xxxhdpi/sticker_32.webp | Bin 4032 -> 0 bytes app/src/main/res/layout/image_editor_hud.xml | 40 ++++++- app/src/main/res/values/strings.xml | 8 ++ app/witness-verifications.gradle | 100 ++++++++++++---- 21 files changed, 601 insertions(+), 71 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/FaceBlurRenderer.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/keyvalue/TooltipValues.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/scribbles/FaceDetector.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/scribbles/FirebaseFaceDetector.java create mode 100644 app/src/main/res/drawable-hdpi/ic_image_editor_blur.png create mode 100644 app/src/main/res/drawable-mdpi/ic_image_editor_blur.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_image_editor_blur.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_image_editor_blur.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_image_editor_blur.png delete mode 100644 app/src/main/res/drawable-xxxhdpi/sticker_32.webp diff --git a/app/build.gradle b/app/build.gradle index dc8838d37..23e6b6df9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -281,9 +281,10 @@ dependencies { implementation "androidx.autofill:autofill:1.0.0" implementation "androidx.paging:paging-common:2.1.2" implementation "androidx.paging:paging-runtime:2.1.2" + implementation 'com.google.firebase:firebase-ml-vision:24.0.3' + implementation 'com.google.firebase:firebase-ml-vision-face-model:20.0.1' - - implementation('com.google.firebase:firebase-messaging:17.3.4') { + implementation ('com.google.firebase:firebase-messaging:20.2.0') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' diff --git a/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmReceiveService.java b/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmReceiveService.java index 1d96a66b8..868d6aeec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmReceiveService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmReceiveService.java @@ -29,7 +29,7 @@ public class FcmReceiveService extends FirebaseMessagingService { if (challenge != null) { handlePushChallenge(challenge); } else { - handleReceivedNotification(getApplicationContext()); + handleReceivedNotification(ApplicationDependencies.getApplication()); } } @@ -37,7 +37,7 @@ public class FcmReceiveService extends FirebaseMessagingService { public void onNewToken(String token) { Log.i(TAG, "onNewToken()"); - if (!TextSecurePreferences.isPushRegistered(getApplicationContext())) { + if (!TextSecurePreferences.isPushRegistered(ApplicationDependencies.getApplication())) { Log.i(TAG, "Got a new FCM token, but the user isn't registered."); return; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorView.java b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorView.java index 750ae7ae6..96f467994 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorView.java @@ -311,7 +311,7 @@ public final class ImageEditorView extends FrameLayout { } private @Nullable EditSession startEdit(@NonNull Matrix inverse, @NonNull PointF point, @Nullable EditorElement selected) { - if (mode == Mode.Draw) { + if (mode == Mode.Draw || mode == Mode.Blur) { return startADrawingSession(point); } else { return startAMoveAndResizeSession(inverse, point, selected); @@ -320,7 +320,7 @@ public final class ImageEditorView extends FrameLayout { private EditSession startADrawingSession(@NonNull PointF point) { BezierDrawingRenderer renderer = new BezierDrawingRenderer(color, thickness * Bounds.FULL_BOUNDS.width(), cap, model.findCropRelativeToRoot()); - EditorElement element = new EditorElement(renderer, EditorModel.Z_DRAWING); + EditorElement element = new EditorElement(renderer, mode == Mode.Blur ? EditorModel.Z_MASK : EditorModel.Z_DRAWING); model.addElementCentered(element, 1); Matrix elementInverseMatrix = model.findElementInverseMatrix(element, viewMatrix); @@ -354,10 +354,10 @@ public final class ImageEditorView extends FrameLayout { this.mode = mode; } - public void startDrawing(float thickness, @NonNull Paint.Cap cap) { + public void startDrawing(float thickness, @NonNull Paint.Cap cap, boolean blur) { this.thickness = thickness; this.cap = cap; - setMode(Mode.Draw); + setMode(blur ? Mode.Blur : Mode.Draw); } public void setDrawingBrushColor(int color) { @@ -448,12 +448,13 @@ public final class ImageEditorView extends FrameLayout { } private boolean allowTaps() { - return !model.isCropping() && mode != Mode.Draw; + return !model.isCropping() && mode != Mode.Draw && mode != Mode.Blur; } public enum Mode { MoveAndResize, - Draw + Draw, + Blur } public interface DrawingChangedListener { diff --git a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java index 7a95f9831..96272b624 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java @@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.imageeditor.Renderer; import org.thoughtcrime.securesms.imageeditor.RendererContext; import org.thoughtcrime.securesms.imageeditor.UndoRedoStackListener; import org.thoughtcrime.securesms.imageeditor.renderers.MultiLineTextRenderer; +import org.thoughtcrime.securesms.imageeditor.renderers.FaceBlurRenderer; import java.util.HashMap; import java.util.LinkedHashSet; @@ -662,7 +663,10 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { */ public void addElement(@NonNull EditorElement element) { pushUndoPoint(); + addElementWithoutPushUndo(element); + } + public void addElementWithoutPushUndo(@NonNull EditorElement element) { EditorElement mainImage = editorElementHierarchy.getMainImage(); EditorElement parent = mainImage != null ? mainImage : editorElementHierarchy.getImageRoot(); @@ -675,6 +679,36 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { updateUndoRedoAvailableState(undoRedoStacks); } + public void clearFaceRenderers() { + EditorElement mainImage = editorElementHierarchy.getMainImage(); + if (mainImage != null) { + boolean hasPushedUndo = false; + for (int i = mainImage.getChildCount() - 1; i >= 0; i--) { + if (mainImage.getChild(i).getRenderer() instanceof FaceBlurRenderer) { + if (!hasPushedUndo) { + pushUndoPoint(); + hasPushedUndo = true; + } + + mainImage.deleteChild(mainImage.getChild(i), invalidate); + } + } + } + } + + public boolean hasFaceRenderer() { + EditorElement mainImage = editorElementHierarchy.getMainImage(); + if (mainImage != null) { + for (int i = mainImage.getChildCount() - 1; i >= 0; i--) { + if (mainImage.getChild(i).getRenderer() instanceof FaceBlurRenderer) { + return true; + } + } + } + + return false; + } + public boolean isChanged() { return undoRedoStacks.isChanged(editorElementHierarchy.getRoot()); } @@ -741,6 +775,10 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { return editorElementHierarchy.getRoot(); } + public EditorElement getMainImage() { + return editorElementHierarchy.getMainImage(); + } + public void delete(@NonNull EditorElement editorElement) { editorElementHierarchy.getImageRoot().forAllInTree(element -> element.deleteChild(editorElement, invalidate)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/FaceBlurRenderer.java b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/FaceBlurRenderer.java new file mode 100644 index 000000000..101c8edf9 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/FaceBlurRenderer.java @@ -0,0 +1,86 @@ +package org.thoughtcrime.securesms.imageeditor.renderers; + +import android.graphics.Matrix; +import android.graphics.Point; +import android.graphics.RectF; +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.imageeditor.Bounds; +import org.thoughtcrime.securesms.imageeditor.Renderer; +import org.thoughtcrime.securesms.imageeditor.RendererContext; +import org.thoughtcrime.securesms.util.ViewUtil; + +/** + * A rectangle that will be rendered on the blur mask layer. Intended for blurring faces. + */ +public class FaceBlurRenderer implements Renderer { + + private static final int CORNER_RADIUS = 0; + + private final RectF faceRect; + private final Point imageDimensions; + private final Matrix scaleMatrix; + + public FaceBlurRenderer(@NonNull RectF faceRect, @NonNull Matrix matrix) { + this.faceRect = faceRect; + this.imageDimensions = new Point(0, 0); + this.scaleMatrix = matrix; + } + + public FaceBlurRenderer(@NonNull RectF faceRect, @NonNull Point imageDimensions) { + this.faceRect = faceRect; + this.imageDimensions = imageDimensions; + this.scaleMatrix = new Matrix(); + + scaleMatrix.setRectToRect(new RectF(0, 0, this.imageDimensions.x, this.imageDimensions.y), Bounds.FULL_BOUNDS, Matrix.ScaleToFit.CENTER); + } + + @Override + public void render(@NonNull RendererContext rendererContext) { + rendererContext.canvas.save(); + rendererContext.canvas.concat(scaleMatrix); + rendererContext.canvas.drawRoundRect(faceRect, CORNER_RADIUS, CORNER_RADIUS, rendererContext.getMaskPaint()); + rendererContext.canvas.restore(); + } + + @Override + public boolean hitTest(float x, float y) { + return false; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeFloat(faceRect.left); + dest.writeFloat(faceRect.top); + dest.writeFloat(faceRect.right); + dest.writeFloat(faceRect.bottom); + dest.writeInt(imageDimensions.x); + dest.writeInt(imageDimensions.y); + } + + public static final Creator CREATOR = new Creator() { + @Override + public FaceBlurRenderer createFromParcel(Parcel in) { + float left = in.readFloat(); + float top = in.readFloat(); + float right = in.readFloat(); + float bottom = in.readFloat(); + int x = in.readInt(); + int y = in.readInt(); + + return new FaceBlurRenderer(new RectF(left, top, right, bottom), new Point(x, y)); + } + + @Override + public FaceBlurRenderer[] newArray(int size) { + return new FaceBlurRenderer[size]; + } + }; +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java index b9e8ff93c..e48f72d05 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.keyvalue; import androidx.annotation.NonNull; +import androidx.lifecycle.OnLifecycleEvent; import androidx.preference.PreferenceDataStore; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; @@ -19,6 +20,7 @@ public final class SignalStore { public static void onFirstEverAppLaunch() { registrationValues().onFirstEverAppLaunch(); uiHints().onFirstEverAppLaunch(); + tooltips().onFirstEverAppLaunch(); } public static @NonNull KbsValues kbsValues() { @@ -41,6 +43,14 @@ public final class SignalStore { return new StorageServiceValues(getStore()); } + public static @NonNull UiHints uiHints() { + return new UiHints(getStore()); + } + + public static @NonNull TooltipValues tooltips() { + return new TooltipValues(getStore()); + } + public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() { return new GroupsV2AuthorizationSignalStoreCache(getStore()); } @@ -61,10 +71,6 @@ public final class SignalStore { putLong(MESSAGE_REQUEST_ENABLE_TIME, time); } - public static UiHints uiHints() { - return new UiHints(getStore()); - } - public static @NonNull PreferenceDataStore getPreferenceDataStore() { return new SignalPreferenceDataStore(getStore()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/TooltipValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/TooltipValues.java new file mode 100644 index 000000000..3b0c09604 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/TooltipValues.java @@ -0,0 +1,36 @@ +package org.thoughtcrime.securesms.keyvalue; + +import androidx.annotation.NonNull; + +import org.whispersystems.signalservice.api.storage.StorageKey; + +public class TooltipValues { + + private static final String BLUR_HUD_ICON = "tooltip.blur_hud_icon"; + private static final String AUTO_BLUR_FACES = "tooltip.auto_blur_faces"; + + private final KeyValueStore store; + + TooltipValues(@NonNull KeyValueStore store) { + this.store = store; + } + + public void onFirstEverAppLaunch() { + } + + public boolean hasSeenBlurHudIconTooltip() { + return store.getBoolean(BLUR_HUD_ICON, false); + } + + public void markBlurHudIconTooltipSeen() { + store.beginWrite().putBoolean(BLUR_HUD_ICON, true).apply(); + } + + public boolean hasSeenAutoBlurFacesTooltip() { + return store.getBoolean(AUTO_BLUR_FACES, false); + } + + public void markAutoBlurFacesTooltipSeen() { + store.beginWrite().putBoolean(AUTO_BLUR_FACES, true).apply(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/FaceDetector.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/FaceDetector.java new file mode 100644 index 000000000..9f3e514cd --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/FaceDetector.java @@ -0,0 +1,10 @@ +package org.thoughtcrime.securesms.scribbles; + +import android.graphics.Bitmap; +import android.graphics.RectF; + +import java.util.List; + +interface FaceDetector { + List detect(Bitmap bitmap); +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/FirebaseFaceDetector.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/FirebaseFaceDetector.java new file mode 100644 index 000000000..6e106b460 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/FirebaseFaceDetector.java @@ -0,0 +1,79 @@ +package org.thoughtcrime.securesms.scribbles; + +import android.graphics.Bitmap; +import android.graphics.RectF; +import android.os.Build; + +import com.annimon.stream.Stream; +import com.google.firebase.ml.vision.FirebaseVision; +import com.google.firebase.ml.vision.common.FirebaseVisionImage; +import com.google.firebase.ml.vision.face.FirebaseVisionFace; +import com.google.firebase.ml.vision.face.FirebaseVisionFaceDetector; +import com.google.firebase.ml.vision.face.FirebaseVisionFaceDetectorOptions; + +import org.thoughtcrime.securesms.logging.Log; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +class FirebaseFaceDetector implements FaceDetector { + + private static final String TAG = Log.tag(FirebaseFaceDetector.class); + + private static final long MAX_SIZE = 1000 * 1000; + + @Override + public List detect(Bitmap source) { + long startTime = System.currentTimeMillis(); + + int performanceMode = getPerformanceMode(source); + Log.d(TAG, "Using performance mode " + performanceMode + " (API " + Build.VERSION.SDK_INT + ", " + source.getWidth() + "x" + source.getHeight() + ")"); + + FirebaseVisionFaceDetectorOptions options = new FirebaseVisionFaceDetectorOptions.Builder() + .setPerformanceMode(performanceMode) + .setMinFaceSize(0.05f) + .setContourMode(FirebaseVisionFaceDetectorOptions.NO_CONTOURS) + .setLandmarkMode(FirebaseVisionFaceDetectorOptions.NO_LANDMARKS) + .setClassificationMode(FirebaseVisionFaceDetectorOptions.NO_CLASSIFICATIONS) + .build(); + + FirebaseVisionImage image = FirebaseVisionImage.fromBitmap(source); + List output = new ArrayList<>(); + + try (FirebaseVisionFaceDetector detector = FirebaseVision.getInstance().getVisionFaceDetector(options)) { + CountDownLatch latch = new CountDownLatch(1); + + detector.detectInImage(image) + .addOnSuccessListener(firebaseVisionFaces -> { + output.addAll(Stream.of(firebaseVisionFaces) + .map(FirebaseVisionFace::getBoundingBox) + .map(r -> new RectF(r.left, r.top, r.right, r.bottom)) + .toList()); + latch.countDown(); + }) + .addOnFailureListener(e -> latch.countDown()); + + latch.await(15, TimeUnit.SECONDS); + } catch (IOException e) { + Log.w(TAG, "Failed to close!", e); + } catch (InterruptedException e) { + Log.w(TAG, e); + } + + Log.d(TAG, "Finished in " + (System.currentTimeMillis() - startTime) + " ms"); + + return output; + } + + private static int getPerformanceMode(Bitmap source) { + if (Build.VERSION.SDK_INT < 28) { + return FirebaseVisionFaceDetectorOptions.FAST; + } + + return source.getWidth() * source.getHeight() < MAX_SIZE ? FirebaseVisionFaceDetectorOptions.ACCURATE + : FirebaseVisionFaceDetectorOptions.FAST; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java index 26ae8debd..c47ebc6ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java @@ -4,6 +4,8 @@ import android.Manifest; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.RectF; import android.net.Uri; import android.os.Bundle; import android.view.LayoutInflater; @@ -13,15 +15,19 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.components.TooltipPopup; 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.imageeditor.renderers.FaceBlurRenderer; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mediasend.MediaSendPageFragment; import org.thoughtcrime.securesms.mms.MediaConstraints; @@ -29,15 +35,18 @@ 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 org.thoughtcrime.securesms.util.views.SimpleProgressDialog; +import org.whispersystems.libsignal.util.Pair; import java.io.ByteArrayOutputStream; +import java.util.Collections; +import java.util.List; import static android.app.Activity.RESULT_OK; @@ -54,6 +63,8 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu private EditorModel restoredModel; + private Pair cachedFaceDetection; + @Nullable private EditorElement currentSelection; private int imageMaxHeight; private int imageMaxWidth; @@ -84,10 +95,10 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu } } - private Uri imageUri; - private Controller controller; - private ImageEditorHud imageEditorHud; - private ImageEditorView imageEditorView; + private Uri imageUri; + private Controller controller; + private ImageEditorHud imageEditorHud; + private ImageEditorView imageEditorView; public static ImageEditorFragment newInstanceForAvatar(@NonNull Uri imageUri) { ImageEditorFragment fragment = newInstance(imageUri); @@ -169,6 +180,11 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu imageEditorView.setModel(editorModel); + if (!SignalStore.tooltips().hasSeenBlurHudIconTooltip()) { + imageEditorHud.showBlurHudTooltip(); + SignalStore.tooltips().markBlurHudIconTooltipSeen(); + } + refreshUniqueColors(); } @@ -279,12 +295,22 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu } case DRAW: { - imageEditorView.startDrawing(0.01f, Paint.Cap.ROUND); + imageEditorView.startDrawing(0.01f, Paint.Cap.ROUND, false); break; } case HIGHLIGHT: { - imageEditorView.startDrawing(0.03f, Paint.Cap.SQUARE); + imageEditorView.startDrawing(0.03f, Paint.Cap.SQUARE, false); + break; + } + + case BLUR: { + imageEditorView.startDrawing(0.055f, Paint.Cap.ROUND, true); + imageEditorHud.setBlurFacesToggleEnabled(imageEditorView.getModel().hasFaceRenderer()); + if (!SignalStore.tooltips().hasSeenAutoBlurFacesTooltip()) { + imageEditorHud.showAutoBlurFacesTooltip(); + SignalStore.tooltips().markAutoBlurFacesTooltipSeen(); + } break; } @@ -316,10 +342,42 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu changeEntityColor(color); } + @Override + public void onBlurFacesToggled(boolean enabled) { + if (!enabled) { + imageEditorView.getModel().clearFaceRenderers(); + return; + } + + if (cachedFaceDetection != null && cachedFaceDetection.first().equals(getUri())) { + renderFaceBlurs(cachedFaceDetection.second()); + return; + } else if (cachedFaceDetection != null && !cachedFaceDetection.first().equals(getUri())) { + cachedFaceDetection = null; + } + + AlertDialog progress = SimpleProgressDialog.show(requireContext()); + + SimpleTask.run(() -> { + Bitmap bitmap = ((UriGlideRenderer) imageEditorView.getModel().getMainImage().getRenderer()).getBitmap(); + + if (bitmap != null) { + FaceDetector detector = new FirebaseFaceDetector(); + return new FaceDetectionResult(detector.detect(bitmap), new Point(bitmap.getWidth(), bitmap.getHeight())); + } else { + return new FaceDetectionResult(Collections.emptyList(), new Point(0, 0)); + } + }, result -> { + renderFaceBlurs(result); + progress.dismiss(); + }); + } + @Override public void onUndo() { imageEditorView.getModel().undo(); refreshUniqueColors(); + imageEditorHud.setBlurFacesToggleEnabled(imageEditorView.getModel().hasFaceRenderer()); } @Override @@ -396,7 +454,30 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu imageEditorHud.setUndoAvailability(undoAvailable); } - private final ImageEditorView.TapListener selectionListener = new ImageEditorView.TapListener() { + private void renderFaceBlurs(@NonNull FaceDetectionResult result) { + List faces = result.rects; + Point size = result.imageSize; + + if (faces.isEmpty()) { + Toast.makeText(requireContext(), R.string.ImageEditorFragment_no_faces_detected, Toast.LENGTH_SHORT).show(); + imageEditorHud.setBlurFacesToggleEnabled(false); + cachedFaceDetection = null; + return; + } + + imageEditorView.getModel().pushUndoPoint(); + + for (RectF face : faces) { + FaceBlurRenderer faceBlurRenderer = new FaceBlurRenderer(face, size); + imageEditorView.getModel().addElementWithoutPushUndo(new EditorElement(faceBlurRenderer, EditorModel.Z_MASK)); + } + + imageEditorView.invalidate(); + + cachedFaceDetection = new Pair<>(getUri(), result); + } + + private final ImageEditorView.TapListener selectionListener = new ImageEditorView.TapListener() { @Override public void onEntityDown(@Nullable EditorElement editorElement) { @@ -449,4 +530,14 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu void onDoneEditing(); } + + private static class FaceDetectionResult { + private final List rects; + private final Point imageSize; + + private FaceDetectionResult(@NonNull List rects, @NonNull Point imageSize) { + this.rects = rects; + this.imageSize = imageSize; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorHud.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorHud.java index a928d895c..50c1c75a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorHud.java +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorHud.java @@ -4,15 +4,19 @@ import android.content.Context; import android.graphics.Color; import android.util.AttributeSet; import android.view.View; +import android.widget.CompoundButton; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.Switch; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.components.TooltipPopup; import org.thoughtcrime.securesms.scribbles.widget.ColorPaletteAdapter; import org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker; @@ -34,6 +38,7 @@ public final class ImageEditorHud extends LinearLayout { private ImageView cropAspectLock; private View drawButton; private View highlightButton; + private View blurButton; private View textButton; private View stickerButton; private View undoButton; @@ -41,6 +46,8 @@ public final class ImageEditorHud extends LinearLayout { private View deleteButton; private View confirmButton; private View doneButton; + private View blurToggleContainer; + private Switch blurToggle; private VerticalSlideColorPicker colorPicker; private RecyclerView colorPalette; @@ -74,21 +81,24 @@ public final class ImageEditorHud extends LinearLayout { 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); - doneButton = findViewById(R.id.scribble_done_button); + 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); + blurButton = findViewById(R.id.scribble_blur_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); + doneButton = findViewById(R.id.scribble_done_button); + blurToggleContainer = findViewById(R.id.scribble_blur_toggle_container); + blurToggle = findViewById(R.id.scribble_blur_toggle); cropAspectLock.setOnClickListener(v -> { eventListener.onCropAspectLock(!eventListener.isCropAspectLocked()); @@ -105,12 +115,14 @@ public final class ImageEditorHud extends LinearLayout { } private void initializeVisibilityMap() { - setVisibleViewsWhenInMode(Mode.NONE, drawButton, highlightButton, textButton, stickerButton, cropButton, undoButton, saveButton); + setVisibleViewsWhenInMode(Mode.NONE, drawButton, blurButton, textButton, stickerButton, cropButton, undoButton, saveButton); setVisibleViewsWhenInMode(Mode.DRAW, confirmButton, undoButton, colorPicker, colorPalette); setVisibleViewsWhenInMode(Mode.HIGHLIGHT, confirmButton, undoButton, colorPicker, colorPalette); + setVisibleViewsWhenInMode(Mode.BLUR, confirmButton, undoButton, blurToggleContainer); + setVisibleViewsWhenInMode(Mode.TEXT, confirmButton, deleteButton, colorPicker, colorPalette); setVisibleViewsWhenInMode(Mode.MOVE_DELETE, confirmButton, deleteButton); @@ -152,11 +164,13 @@ public final class ImageEditorHud extends LinearLayout { colorPalette.setAdapter(colorPaletteAdapter); drawButton.setOnClickListener(v -> setMode(Mode.DRAW)); + blurButton.setOnClickListener(v -> setMode(Mode.BLUR)); highlightButton.setOnClickListener(v -> setMode(Mode.HIGHLIGHT)); textButton.setOnClickListener(v -> setMode(Mode.TEXT)); stickerButton.setOnClickListener(v -> setMode(Mode.INSERT_STICKER)); saveButton.setOnClickListener(v -> eventListener.onSave()); doneButton.setOnClickListener(v -> eventListener.onDone()); + blurToggle.setOnCheckedChangeListener((button, enabled) -> eventListener.onBlurFacesToggled(enabled)); } public void setUpForAvatarEditing() { @@ -186,6 +200,26 @@ public final class ImageEditorHud extends LinearLayout { colorPicker.setActiveColor(color); } + public void setBlurFacesToggleEnabled(boolean enabled) { + blurToggle.setChecked(enabled); + } + + public void showBlurHudTooltip() { + TooltipPopup.forTarget(blurButton) + .setText(R.string.ImageEditorHud_new_auto_blur_faces_and_blur_brush) + .setBackgroundTint(ContextCompat.getColor(getContext(), R.color.core_ultramarine)) + .setTextColor(ContextCompat.getColor(getContext(), R.color.core_white)) + .show(TooltipPopup.POSITION_BELOW); + } + + public void showAutoBlurFacesTooltip() { + TooltipPopup.forTarget(blurToggleContainer) + .setText(R.string.ImageEditorHud_draw_to_blur_or_try_auto_blur) + .setBackgroundTint(ContextCompat.getColor(getContext(), R.color.core_ultramarine)) + .setTextColor(ContextCompat.getColor(getContext(), R.color.core_white)) + .show(TooltipPopup.POSITION_ABOVE); + } + public void setEventListener(@Nullable EventListener eventListener) { this.eventListener = eventListener != null ? eventListener : NULL_EVENT_LISTENER; } @@ -267,6 +301,7 @@ public final class ImageEditorHud extends LinearLayout { TEXT, DRAW, HIGHLIGHT, + BLUR, MOVE_DELETE, INSERT_STICKER, } @@ -274,6 +309,7 @@ public final class ImageEditorHud extends LinearLayout { public interface EventListener { void onModeStarted(@NonNull Mode mode); void onColorChange(int color); + void onBlurFacesToggled(boolean enabled); void onUndo(); void onDelete(); void onSave(); @@ -295,6 +331,10 @@ public final class ImageEditorHud extends LinearLayout { public void onColorChange(int color) { } + @Override + public void onBlurFacesToggled(boolean enabled) { + } + @Override public void onUndo() { } diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java index faf51e34b..9c84788e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java @@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.scribbles; import android.content.Context; import android.graphics.Bitmap; -import android.graphics.BlurMaskFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Point; @@ -29,9 +28,11 @@ import org.thoughtcrime.securesms.imageeditor.Renderer; import org.thoughtcrime.securesms.imageeditor.RendererContext; import org.thoughtcrime.securesms.imageeditor.model.EditorElement; import org.thoughtcrime.securesms.imageeditor.model.EditorModel; +import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideRequest; +import org.thoughtcrime.securesms.util.BitmapUtil; import java.util.concurrent.ExecutionException; @@ -42,12 +43,16 @@ import java.util.concurrent.ExecutionException; */ final class UriGlideRenderer implements Renderer { + private static final String TAG = Log.tag(UriGlideRenderer.class); + private static final int PREVIEW_DIMENSION_LIMIT = 2048; + private static final int MAX_BLUR_DIMENSION = 300; private final Uri imageUri; private final Paint paint = new Paint(); private final Matrix imageProjectionMatrix = new Matrix(); private final Matrix temp = new Matrix(); + private final Matrix blurScaleMatrix = new Matrix(); private final boolean decryptable; private final int maxWidth; private final int maxHeight; @@ -55,7 +60,6 @@ final class UriGlideRenderer implements Renderer { @Nullable private Bitmap bitmap; @Nullable private Bitmap blurredBitmap; @Nullable private Paint blurPaint; - @Nullable private BlurMaskFilter blurMaskFilter; UriGlideRenderer(@NonNull Uri imageUri, boolean decryptable, int maxWidth, int maxHeight) { this.imageUri = imageUri; @@ -124,12 +128,8 @@ final class UriGlideRenderer implements Renderer { for (EditorElement child : rendererContext.getChildren()) { if (child.getZOrder() == EditorModel.Z_MASK) { renderMask = true; - if (blurMaskFilter == null) { - blurMaskFilter = new BlurMaskFilter(4, BlurMaskFilter.Blur.NORMAL); // This blurs edges of the mask shapes - } if (blurPaint == null) { blurPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - blurPaint.setMaskFilter(blurMaskFilter); } blurPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); rendererContext.setMaskPaint(blurPaint); @@ -143,7 +143,16 @@ final class UriGlideRenderer implements Renderer { blurPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP)); blurPaint.setMaskFilter(null); - if (blurredBitmap == null) blurredBitmap = blur(bitmap, rendererContext.context); + + if (blurredBitmap == null) { + blurredBitmap = blur(bitmap, rendererContext.context); + + blurScaleMatrix.setRectToRect(new RectF(0, 0, blurredBitmap.getWidth(), blurredBitmap.getHeight()), + new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight()), + Matrix.ScaleToFit.FILL); + } + + rendererContext.canvas.concat(blurScaleMatrix); rendererContext.canvas.drawBitmap(blurredBitmap, 0, 0, blurPaint); blurPaint.setXfermode(null); @@ -197,7 +206,7 @@ final class UriGlideRenderer implements Renderer { * 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() { + public @Nullable Bitmap getBitmap() { if (bitmap != null && bitmap.isRecycled()) { bitmap = null; } @@ -223,21 +232,48 @@ final class UriGlideRenderer implements Renderer { return matrix; } - private static @NonNull Bitmap blur(@NonNull Bitmap bitmap, @NonNull Context context) { + private static @NonNull Bitmap blur(Bitmap bitmap, Context context) { + Point previewSize = scaleKeepingAspectRatio(new Point(bitmap.getWidth(), bitmap.getHeight()), PREVIEW_DIMENSION_LIMIT); + Point blurSize = scaleKeepingAspectRatio(new Point(previewSize.x / 2, previewSize.y / 2 ), MAX_BLUR_DIMENSION); + Bitmap small = BitmapUtil.createScaledBitmap(bitmap, blurSize.x, blurSize.y); + + Log.d(TAG, "Bitmap: " + bitmap.getWidth() + "x" + bitmap.getHeight() + ", Blur: " + blurSize.x + "x" + blurSize.y); + RenderScript rs = RenderScript.create(context); - Allocation input = Allocation.createFromBitmap(rs, bitmap); - Allocation output = Allocation.createTyped (rs, input.getType()); + Allocation input = Allocation.createFromBitmap(rs, small); + Allocation output = Allocation.createTyped(rs, input.getType()); ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); script.setRadius(25f); script.setInput(input); script.forEach(output); - Bitmap blurred = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig()); + Bitmap blurred = Bitmap.createBitmap(small.getWidth(), small.getHeight(), small.getConfig()); output.copyTo(blurred); return blurred; } + private static @NonNull Point scaleKeepingAspectRatio(@NonNull Point dimens, int maxDimen) { + int outX = dimens.x; + int outY = dimens.y; + + if (dimens.x > maxDimen || dimens.y > maxDimen) { + outX = maxDimen; + outY = maxDimen; + + float widthRatio = dimens.x / (float) maxDimen; + float heightRatio = dimens.y / (float) maxDimen; + + if (widthRatio > heightRatio) { + outY = (int) (dimens.y / widthRatio); + } else { + outX = (int) (dimens.x / heightRatio); + } + } + + return new Point(outX, outY); + } + public static final Creator CREATOR = new Creator() { @Override public UriGlideRenderer createFromParcel(Parcel in) { diff --git a/app/src/main/res/drawable-hdpi/ic_image_editor_blur.png b/app/src/main/res/drawable-hdpi/ic_image_editor_blur.png new file mode 100644 index 0000000000000000000000000000000000000000..77606a7782d184aabeb755dd37c3aec3e1a3ad5a GIT binary patch literal 1800 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_image_editor_blur.png b/app/src/main/res/drawable-xxxhdpi/ic_image_editor_blur.png new file mode 100644 index 0000000000000000000000000000000000000000..f5a893e97f491701174f6c99b838bd32da9d2eb6 GIT binary patch literal 6738 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/sticker_32.webp b/app/src/main/res/drawable-xxxhdpi/sticker_32.webp deleted file mode 100644 index 7bda7018b7c511b41b99af602eb6dd1ea8dcabcc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4032 zcmV;x4?plyNk&Gv4*&pHMM6+kP&iDi4*&o!f50CQO)zL1$&s?N-@~8q6+MKA{!aiu zMjiA#fkc^iU+@?mwEG4%6WV$U%P_a9s4AAVqk^uY3ZFBVdBCz(W^yeO;OCL~Rv$U3tp+qF)Pl#xBwcvc&01<8@Nm8V#KJA%h*3f@j-VvOd z0Q=jv9gig4rBPdBuWj2l$F^}^T~o8un66av!}or@HTVOrJWuXQ1U+oq z4L9ERoij5*9n`7qHbu0yZQJ{^{iki)w$XNN)%GIE7L%l%Iddl9;D1I8z0@Vgmxg_b zh=rc?@QZsgEcS6}TD2OI-=3kZZB6=|phSewFD-^KW~9#uPAE~k7xLI8doZliZOm4r z_X}=Hxuu2lKz}q(*RW>(vWZCV)x*uLryldXCs)t&dEB-{q<5mybCOrz<8j8rynTf2 zBr$e)71$*HZE4T5O|woiO*9RJ4H*0N{e~WzJtT!=B;H~zt=&=^p+;~d_>{cF_dnmz z_4|5%-gVQ!GA30FDM+*gQIQl1qW8g_II)d6ApLW$4GJdZ@GKma7c@S2;r6;S+9D*S zgsx~?B=1vi9nQoF&yy~m`<6w5XYt~mUw*UmkCIYiMM&?Hx{cG~?}VB6sOc200C1nX~e0p8W$+uUxwCBnju<(J;8=q1H{Kf*bh_a zWV$MyCa!B-+b|<+?Wi@(Gt4s+r=?6u?bFr4^t{(4i(-bvh9pK|sJ5fx)DmnNx(laz9x0vdL4^Yv|Lj7thmq#}wL$XIOeDwX@IM`1$c( zxm2#K6OV@tb9K->4)b%`zH{#HM-PhLeD%6jF3>|~Ss)^$(fOT1gtYB%&ZGA6&U2o- zRFX#}i1Xrpsrk|2LoQ$c?Pl^px0GP;2GTmwi4Jugg_Gpsik73)RQ~+$Rwv|9uDAm~ zwBCTVhj8Fi+RlIdkN-9&N-g%#khJJ@I-PQTLBu3GBs8~E91Z=lVOWtfLl8o^Y4>2R z4?5lGQ1xPMvGU7?MySAO==XhMre-lSgzwXlxS8@6Td_h#=?$tYj?ktKX#p zhfb+$$cUJCMXa`_(5HW)$smrgjY+Uy3T6m`hzQ^X<~~DVWcbWFsZuy~bZ~K=nlqLQ zs=eZy3eo~FiLo_rI~oy01c37``MqL=zkK&&pC&pUyQJxn=SdT06&~-{=A@hgu+tm! zf%7B+{E6?btD2fodF9gQg-P@QM;D(+_~N;O;u+7$lVI>k2m%&sm%hHwt(AeOix)|h za5$B#lbdrG)>#xUK;MI@L7*2Gj@#=ykTMCAGLA`0GrPb zA^`qTTiCEv0^2nqtb&tr3g0=cGQh^4M-Tyg_jF-a{wnkz+>>h$(k5|Ko1t^Mlr1jA z*nL_=5QNWZ8Xd3ng>91fckk`=wo4Ae+tLb;_YSLZvV6yNnnlWRPldOoWoFupk$9f1 zFx1n51AT!bL#JDKo-H$lW0Tb~&01L4Ur_tJwRu?BUzldibXs8lnOICV6$ZVPL%!uk zXV6=iWD?o}+guwamhmAsy)r%Q+l8L=@N#DK#UcwKkG(Qa_tW-W(zFxYqldg6 zv-!?JY1NWvy)EiAN~*d(L0 zA&fV~@i!zFp1gK89OMgzZA6UI81OpcPU){m*}yg}ZbHnerd1;Q4T7h4@zB}fK%W)a zSOll39G(3M_Xb6+6rPz6H=PUj@$hMnwv#kAFEd^+?+uFt&see3e>=}|qJ7fPBWco~ zFde(3X9~~Yxo7SkI-7%}PkL?1ERRq={(pxth_X%gD2hE0yFx%c-Sj%woIBz6281Gb$utCC;sP>x_l8-c-=`Q3AS z9~^kt_ZQ#qhUakUq?ah#me3XHcDuHuu1_rAd8VMePWfx2+=W9vH*su4oHozXed}IU zaJcgywRax7ct9DlDM|5)QM95Y2nEKr2)51h4Yw?#womu*RMBpGgRa^OzKpRGoBVjp zQ4dfXkDa>Z%VYg=sY+==0Ee+{L`a)Byx?<7scnSj1w~S{)Kx)g`!?tSg@VAm!!$ zyI$YdE4Pw_;$R0h*iKjBfb4CzQQK1gd-8|A2mwNG)D=Isw88{pbU_%4*!1I%?xH;J z(iflCWYvTqG7M>q)8&{(zrMDWOYLy?$xlA1h*j|X?$%<3i2|3O;Ns8MQU34WpZZm9 zg#$utV?;=o@$RYn9;8;`m-9q|yrcG=7*O%vg8L9o+!FI&r9`r}t$YLQb^ z078@4r0pb)y$XD5S<_AJlA-gTyeUOhk_rLiaFMlW1PkjKgiLF%al(n+4+kBlc;Ek< zG{|{s2e&eIVkc>vEn-i&T~L4TZ(DpWhl&xpt8Tx1q!B8tWkk#$hN|2%kK)3HPyZ-a zpz4HlMy8z@zA|TYA+^i>H$8p4K~*vc5atZ-^czQyBZ0znW7@Jmpd@Ak-3!(g8i5_2 zl1HMrlSA&Frk&XFVbkS@sehz?hqvTZg}4Rw$t~qq_b?(C){Hi${Xv|Nj!R}!{NB6E zCMzcLpze|dY`D%dWv!sLA%FFyy@pk-NO2K_N$zUDV_57ftZk}1?xohv&`;jHq%hdD zeWM)WLeyQ|uI2T{oAxEO^Uv+`;!>CFh$lijOMZ7Wp70f>SwwK0*V|3lSGp)(^^QD% z@9M6|G`9G;Wv|{PsBMn_`0dSGR4uE3BuUaCzkAfyJbt4vE1}TVD_WJo`{xyg+D^Qw z60p0g?$TgyBz|SP5~y9&eALr_cgrpmU=fox`TZl~-k!qr6o;+7q(r_--^wa{`%8HQ zkapkw%+cAOJw$Db{q~bhj;cnn2i-z-t3DA+!I+?cnVaYO&8TASUf&?$6;m}Ti96PgO%So9|8xlB8_6! z$uIo*jLN_kNvBCeKYnXTb1Jn zLBveYTH>uKg`sYxSOlljw)lG@uepW#$BymvhMcMpAT&;s;>kPDoF*lH{dHm8noWjJ z>##t);#st?R%~I>fRYf3q;131jx{$^I~w@LE4y4*wPHeOL~PPzJfoa8lPYDGpGHP1 zKIoQnUR%aN9P`-V9;TTKgEtgw+nM;4`rG!(K<$Ey2R=XOk{xjogh|?%XLwH~O3E8I z4@6S^!#~SZF=-))BG`e2Nv6VVpR6Kwg163Hw_H%$RDSl(=2uj$IN|Qfo$~%cqNIF} z`ISox&GDZ#NQ)2z5kUmw023k$b7e&fErQn)zqxfaE49OIM?d)ox9lQtAJnb9e^C;; z61)3O8i=gW2>$0L2#E+H7z_psh1nrR354c=%= z+kMmQ0?XwJ;zST;7z0w_k#R+cvMTw;@|ImtyLj+hNAAi~rKnI~`$Uz`EVSEg3GJOz zrsozI2zz048pLau(JvL|aTx@ki#ua4wc-z*y}C_Ic_6GhQQAjPGDX|%_Un>*^#ukD zZ_WSo&&#+FKzwW_`e|VZBwassV5*>YnfJ%__XftL3WP?)MW34^XK}rR9$KJRcSYeD zubtH?3FNft@$qpX7O`>Z_%Tn@=`=IVC1&IvcFULKul=;! znFa_7w$stW3OCVGpLm5Oaa+8upzi3kh5Emo*59{l@1-#c~UmBorULQ#p* zN#E&J3oGes=A7L9H91uw6xeiMxn`?&Z@9$D}#&yDzu+Uk+u8ryz0K*uH57A*~V;En$;{t(-5f zS+k4=SwOAUz<+-h{TbS?YQzGi*~6 z`wcx}uh1t5m&;H2=ePgk!pUcb%Vk%bh#*sFlh|1NyF4x7bbry`|Nizm!%26sGdlCL zKiBg-vpU8+7UK?jk@oCDCtM`y(04$f7xA6R+Kxp^3@Y# mW0ka~a!dL8(O<@%JWVUPY2WVY+K`?giuqS@j6D#O|5*}rt0)5i diff --git a/app/src/main/res/layout/image_editor_hud.xml b/app/src/main/res/layout/image_editor_hud.xml index 7428388af..e617a9489 100644 --- a/app/src/main/res/layout/image_editor_hud.xml +++ b/app/src/main/res/layout/image_editor_hud.xml @@ -71,7 +71,16 @@ android:layout_height="wrap_content" android:background="?attr/selectableItemBackgroundBorderless" android:padding="8dp" - android:src="@drawable/ic_brush_highlight_32" /> + android:src="@drawable/ic_brush_highlight_32" + android:visibility="gone"/> + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 26040bb76..ade5c147a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -558,6 +558,14 @@ Group avatar Avatar + + No faces detected + + + Auto-blur faces + New: Auto-blur faces and blur brush + Draw to blur, or try auto-blur + Tap and hold to record a voice message, release to send diff --git a/app/witness-verifications.gradle b/app/witness-verifications.gradle index 00c9bba2c..3d001505c 100644 --- a/app/witness-verifications.gradle +++ b/app/witness-verifications.gradle @@ -240,6 +240,15 @@ dependencyVerification { ['com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2', '8dc6a29a5a8db7b2ad5a9a7fda1dc9ae0893f4c8f0545732b2c63854ea693e8e'], + ['com.google.android.datatransport:transport-api:2.2.0', + '576514f8b75d8ae32897f1b9b031f88b00465bf6e0996e227d09af688195f71e'], + + ['com.google.android.datatransport:transport-backend-cct:2.2.0', + '33abba2b7749479ae397176ae482b1807010b2bb331d61264bbdcc799eb398cd'], + + ['com.google.android.datatransport:transport-runtime:2.2.0', + 'e72912014b67151b689a7e820d3f1edf12fe2af5fbc308ab196ac392436ab771'], + ['com.google.android.exoplayer:exoplayer-core:2.9.1', 'b6ab34abac36bc2bc6934b7a50008162feca2c0fde91aaf1e8c1c22f2c16e2c0'], @@ -249,26 +258,47 @@ dependencyVerification { ['com.google.android.gms:play-services-auth-api-phone:16.0.0', '19365818b9ceb048ef48db12b5ffadd5eb86dbeb2c7c7b823bfdd89c665f42e5'], - ['com.google.android.gms:play-services-auth-base:16.0.0', - '51dc02ad2f8d1d9dff7b5b52c4df2c6c12ef7df55d752e919d5cb4dd6002ecd0'], + ['com.google.android.gms:play-services-auth-base:17.0.0', + 'c494d23d5cdc7e4c33721877592868d3dc16085cab535c3f589c03052524f737'], ['com.google.android.gms:play-services-auth:16.0.1', 'aec9e1c584d442cb9f59481a50b2c66dc191872607c04d97ecb82dd0eb5149ec'], - ['com.google.android.gms:play-services-base:16.0.1', - 'aca10c780c3219bc50f3db06734f4ab88badd3113c564c0a3156ff8ff674655b'], + ['com.google.android.gms:play-services-base:17.0.0', + 'dd0980edf729e0d346e2b58e70801dc237c1aed0c7ab274fa3f1c8c8efc64cc7'], - ['com.google.android.gms:play-services-basement:16.0.1', - 'e08bfd1e87c4e50ef76161d7ac76b873aeb975367eeb3afa4abe62ea1887c7c6'], + ['com.google.android.gms:play-services-basement:17.0.0', + 'd324a1785bbc48bfe3639fc847cfd3cf43d49e967b5caf2794240a854557a39c'], + + ['com.google.android.gms:play-services-clearcut:17.0.0', + 'cce72073c269c2b4cff301304751f2faa2cd1b0344fef581a59da63665f9a4b4'], + + ['com.google.android.gms:play-services-flags:17.0.0', + '746e66b850c5d2b3a0c73871d3fe71ad1b98b62abc0625bbd5badabb73c82cf2'], ['com.google.android.gms:play-services-maps:16.1.0', 'ff50cae9e4059416202375597d99cdc8ddefd9cea3f1dc2ff53779a3a12eb480'], - ['com.google.android.gms:play-services-stats:16.0.1', - '5b2d8281adbfd6e74d2295c94bab9ea80fc9a84dfbb397995673f5af4d4c6368'], + ['com.google.android.gms:play-services-phenotype:17.0.0', + '53d40a205e48ad4e35923a01f04d9850acbd7403b3d30fb388e586fad1540ece'], - ['com.google.android.gms:play-services-tasks:16.0.1', - 'b31c18d8d1cc8d9814f295ee7435471333f370ba5bd904ca14f8f2bec4f35c35'], + ['com.google.android.gms:play-services-stats:17.0.0', + 'e8ae5b40512b71e2258bfacd8cd3da398733aa4cde3b32d056093f832b83a6fe'], + + ['com.google.android.gms:play-services-tasks:17.0.0', + '2e6d1738b73647f3fe7a038b9780b97717b3746eae258009197e36e7bf3112a5'], + + ['com.google.android.gms:play-services-vision-common:19.0.2', + 'b1d93b40a8b49d63d86dfd88ddc4030ab7231d839c5ff3adeb876de94d44b970'], + + ['com.google.android.gms:play-services-vision-face-contour-internal:16.0.0', + '79e5be6ea321a7c10822f190c45612f1999d37c7bc846d8b01a35478eeb0f985'], + + ['com.google.android.gms:play-services-vision-image-label:18.0.3', + 'aea181d214e170a07f13f537c165750cf81fe4522c4e3df6a845b9aa1dcaa06d'], + + ['com.google.android.gms:play-services-vision:20.0.0', + '0386c1c32b06c3c771dd518220d47bb5828fa3d415863ecd6859909b52cc4f6f'], ['com.google.android.material:material:1.1.0', '58f4fb6e5986ec8e01a733ea85e9df83cf79060e0329fe18abc192d9eda97b26'], @@ -276,20 +306,47 @@ dependencyVerification { ['com.google.android:flexbox:0.3.0', 'a9989fd13ae2ee42765dfc515fe362edf4f326e74925d02a10369df8092a4935'], - ['com.google.auto.value:auto-value-annotations:1.6.3', - '0e951fee8c31f60270bc46553a8586001b7b93dbb12aec06373aa99a150392c0'], + ['com.google.auto.value:auto-value-annotations:1.6.5', + '3677f725f5b1b6cd6a4cc8aa8cf8f5fd2b76d170205cbdc3e9bfd9b58f934b3b'], - ['com.google.firebase:firebase-common:16.0.3', - '3db6bfd4c6f758551e5f9acdeada2050577277e6da1aefb2412de23829759bcf'], + ['com.google.dagger:dagger:2.24', + '550a6e46a6dfcdf1d764887b6090cea94f783327e50e5c73754f18facfc70b64'], - ['com.google.firebase:firebase-iid-interop:16.0.1', - '2a86322b9346fd4836219206d249e85803311655e96036a8e4b714ce7e79693b'], + ['com.google.firebase:firebase-common:19.3.0', + '7bd7971470ff943e3c3abb1d7809ef5cb4b81f1996be0867714372b3efa7405a'], - ['com.google.firebase:firebase-iid:17.0.4', - 'bb42774e309d5eac1aa493d19711032bee4f677a409639b6a5cfa93089af93eb'], + ['com.google.firebase:firebase-components:16.0.0', + '8ef43b412de4ec3e36a87c66d8a0a14a3de0a2e8566946da6a0e799b7fdd8ec9'], - ['com.google.firebase:firebase-messaging:17.3.4', - 'e42288e7950d7d3b033d3395a5ac9365d230da3e439a2794ec13e2ef0fbaf078'], + ['com.google.firebase:firebase-datatransport:17.0.3', + '10c9f65c4f897ea33d028e46226daaabdfee43ac712559e5570d21b6b58a067e'], + + ['com.google.firebase:firebase-encoders-json:16.0.0', + 'd1769fcec2a424ee7f92b9996c4b5c1dff0dfa27ceed28981b857b144fb5ec49'], + + ['com.google.firebase:firebase-iid-interop:17.0.0', + 'b6f4ad581eb489370be3bf38a4bdabfc6ea3d4e716234c625a0f42516c53523c'], + + ['com.google.firebase:firebase-iid:20.2.0', + '1b6977f8ce19becd20b5a1055347e085490d556b4ef98f6666cb25af1d74ff9b'], + + ['com.google.firebase:firebase-installations-interop:16.0.0', + 'd498fe20e7d2c65fc8f7124f1c1791d2828bebdf6bf06ab4cdee13e7fe9ccaa2'], + + ['com.google.firebase:firebase-installations:16.3.1', + '20427c6899bcbc0390988c958ab7da0352ba84a869817cb6ae9da3b19892af9f'], + + ['com.google.firebase:firebase-messaging:20.2.0', + 'f49cfba49ab33c6fb7436fe9b790b16d3f1265a29955b48fccc1fb1f231da2d8'], + + ['com.google.firebase:firebase-ml-common:22.1.1', + '74ac365da2578a07b7dd5cd6ca4ae6d7279c7010153025d081afa5db0dce6d57'], + + ['com.google.firebase:firebase-ml-vision-face-model:20.0.1', + 'e81fc985d9e680be0b18891fa8d108f546173c5da2fd923d787fd13759db3b8a'], + + ['com.google.firebase:firebase-ml-vision:24.0.3', + 'afe0d27eebcb8c52a1e40f1e147b750456e7e02747b7e8f3b9d7f3aa58922c78'], ['com.google.guava:listenablefuture:1.0', 'e4ad7607e5c0477c6f890ef26a49cb8d1bb4dffb650bab4502afee64644e3069'], @@ -345,6 +402,9 @@ dependencyVerification { ['dnsjava:dnsjava:2.1.9', '072bba34267ffad8907c30a99a6b68f900782f3191454d278e395e289d478446'], + ['javax.inject:javax.inject:1', + '91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff'], + ['me.leolin:ShortcutBadger:1.1.16', 'e3cb3e7625892129b0c92dd5e4bc649faffdd526d5af26d9c45ee31ff8851774'],