diff --git a/app/build.gradle b/app/build.gradle index dc8838d376..23e6b6df9a 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 1d96a66b81..868d6aeec9 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 750ae7ae68..96f467994a 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 7a95f9831f..96272b624a 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 0000000000..101c8edf96 --- /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 b9e8ff93c8..e48f72d058 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 0000000000..3b0c09604f --- /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 0000000000..9f3e514cd2 --- /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 0000000000..6e106b4600 --- /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 26ae8debde..c47ebc6cad 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 a928d895cc..50c1c75a64 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 faf51e34b1..9c84788e08 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 0000000000..77606a7782 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_image_editor_blur.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_image_editor_blur.png b/app/src/main/res/drawable-mdpi/ic_image_editor_blur.png new file mode 100644 index 0000000000..4ab4864cff Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_image_editor_blur.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_image_editor_blur.png b/app/src/main/res/drawable-xhdpi/ic_image_editor_blur.png new file mode 100644 index 0000000000..4a7bcac0c0 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_image_editor_blur.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_image_editor_blur.png b/app/src/main/res/drawable-xxhdpi/ic_image_editor_blur.png new file mode 100644 index 0000000000..613d1db913 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_image_editor_blur.png differ 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 0000000000..f5a893e97f Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_image_editor_blur.png differ 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 7bda7018b7..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/sticker_32.webp and /dev/null differ diff --git a/app/src/main/res/layout/image_editor_hud.xml b/app/src/main/res/layout/image_editor_hud.xml index 7428388af1..e617a94892 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 26040bb761..ade5c147ad 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 00c9bba2c0..3d001505cc 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'],