mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-24 16:57:50 +00:00
Fix camera scaling issues on some phones.
Some phones, notably the Pixel 3, had some problems with scaling after taking photos. This fixes it by using the takePicture API instead of pulling the bitmap from the TextureView. Fixes #8292
This commit is contained in:
parent
76054a9e33
commit
91db26437d
@ -7,7 +7,6 @@ import android.view.Surface;
|
|||||||
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -16,6 +15,9 @@ public class Camera1Controller {
|
|||||||
|
|
||||||
private static final String TAG = Camera1Controller.class.getSimpleName();
|
private static final String TAG = Camera1Controller.class.getSimpleName();
|
||||||
|
|
||||||
|
private final int screenWidth;
|
||||||
|
private final int screenHeight;
|
||||||
|
|
||||||
private Camera camera;
|
private Camera camera;
|
||||||
private int cameraId;
|
private int cameraId;
|
||||||
private OrderEnforcer<Stage> enforcer;
|
private OrderEnforcer<Stage> enforcer;
|
||||||
@ -23,10 +25,12 @@ public class Camera1Controller {
|
|||||||
private SurfaceTexture previewSurface;
|
private SurfaceTexture previewSurface;
|
||||||
private int screenRotation;
|
private int screenRotation;
|
||||||
|
|
||||||
public Camera1Controller(int preferredDirection, @NonNull EventListener eventListener) {
|
public Camera1Controller(int preferredDirection, int screenWidth, int screenHeight, @NonNull EventListener eventListener) {
|
||||||
this.eventListener = eventListener;
|
this.eventListener = eventListener;
|
||||||
this.enforcer = new OrderEnforcer<>(Stage.INITIALIZED, Stage.PREVIEW_STARTED);
|
this.enforcer = new OrderEnforcer<>(Stage.INITIALIZED, Stage.PREVIEW_STARTED);
|
||||||
this.cameraId = Camera.getNumberOfCameras() > 1 ? preferredDirection : Camera.CameraInfo.CAMERA_FACING_BACK;
|
this.cameraId = Camera.getNumberOfCameras() > 1 ? preferredDirection : Camera.CameraInfo.CAMERA_FACING_BACK;
|
||||||
|
this.screenWidth = screenWidth;
|
||||||
|
this.screenHeight = screenHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
@ -49,11 +53,18 @@ public class Camera1Controller {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Camera.Parameters params = camera.getParameters();
|
Camera.Parameters params = camera.getParameters();
|
||||||
Camera.Size maxSize = getMaxSupportedPreviewSize(camera);
|
Camera.Size previewSize = getClosestSize(camera.getParameters().getSupportedPreviewSizes(), screenWidth, screenHeight);
|
||||||
final List<String> focusModes = params.getSupportedFocusModes();
|
Camera.Size pictureSize = getClosestSize(camera.getParameters().getSupportedPictureSizes(), screenWidth, screenHeight);
|
||||||
|
final List<String> focusModes = params.getSupportedFocusModes();
|
||||||
|
|
||||||
params.setPreviewSize(maxSize.width, maxSize.height);
|
Log.d(TAG, "Preview size: " + previewSize.width + "x" + previewSize.height + " Picture size: " + pictureSize.width + "x" + pictureSize.height);
|
||||||
|
|
||||||
|
params.setPreviewSize(previewSize.width, previewSize.height);
|
||||||
|
params.setPictureSize(pictureSize.width, pictureSize.height);
|
||||||
|
params.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
|
||||||
|
params.setColorEffect(Camera.Parameters.EFFECT_NONE);
|
||||||
|
params.setWhiteBalance(Camera.Parameters.WHITE_BALANCE_AUTO);
|
||||||
|
|
||||||
if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
|
if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
|
||||||
params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
|
params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
|
||||||
@ -61,6 +72,7 @@ public class Camera1Controller {
|
|||||||
params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
|
params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
camera.setParameters(params);
|
camera.setParameters(params);
|
||||||
|
|
||||||
enforcer.markCompleted(Stage.INITIALIZED);
|
enforcer.markCompleted(Stage.INITIALIZED);
|
||||||
@ -96,6 +108,14 @@ public class Camera1Controller {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void capture(@NonNull CaptureCallback callback) {
|
||||||
|
enforcer.run(Stage.PREVIEW_STARTED, () -> {
|
||||||
|
camera.takePicture(null, null, null, (data, camera) -> {
|
||||||
|
callback.onCaptureAvailable(data, cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public int flip() {
|
public int flip() {
|
||||||
Log.d(TAG, "flip()");
|
Log.d(TAG, "flip()");
|
||||||
SurfaceTexture surfaceTexture = previewSurface;
|
SurfaceTexture surfaceTexture = previewSurface;
|
||||||
@ -115,13 +135,15 @@ public class Camera1Controller {
|
|||||||
Log.d(TAG, "setScreenRotation(" + screenRotation + ") executing");
|
Log.d(TAG, "setScreenRotation(" + screenRotation + ") executing");
|
||||||
this.screenRotation = screenRotation;
|
this.screenRotation = screenRotation;
|
||||||
|
|
||||||
int rotation = getCameraRotationForScreen(screenRotation);
|
int previewRotation = getPreviewRotation(screenRotation);
|
||||||
camera.setDisplayOrientation(rotation);
|
int outputRotation = getOutputRotation(screenRotation);
|
||||||
|
|
||||||
Log.d(TAG, "Set camera rotation to: " + rotation);
|
Log.d(TAG, "Preview rotation: " + previewRotation + " Output rotation: " + outputRotation);
|
||||||
|
|
||||||
|
camera.setDisplayOrientation(previewRotation);
|
||||||
|
|
||||||
Camera.Parameters params = camera.getParameters();
|
Camera.Parameters params = camera.getParameters();
|
||||||
params.setRotation(rotation);
|
params.setRotation(outputRotation);
|
||||||
camera.setParameters(params);
|
camera.setParameters(params);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -136,33 +158,58 @@ public class Camera1Controller {
|
|||||||
return new Properties(Camera.getNumberOfCameras(), previewSize.width, previewSize.height);
|
return new Properties(Camera.getNumberOfCameras(), previewSize.width, previewSize.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Camera.Size getMaxSupportedPreviewSize(Camera camera) {
|
private Camera.Size getClosestSize(List<Camera.Size> sizes, int width, int height) {
|
||||||
List<Camera.Size> cameraSizes = camera.getParameters().getSupportedPreviewSizes();
|
Collections.sort(sizes, ASC_SIZE_COMPARATOR);
|
||||||
Collections.sort(cameraSizes, DESC_SIZE_COMPARATOR);
|
|
||||||
return cameraSizes.get(0);
|
int i = 0;
|
||||||
|
while (i < sizes.size() && (sizes.get(i).width * sizes.get(i).height) < (width * height)) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sizes.get(Math.min(i, sizes.size() - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getCameraRotationForScreen(int screenRotation) {
|
private int getOutputRotation(int displayRotationCode) {
|
||||||
int degrees = 0;
|
int degrees = convertRotationToDegrees(displayRotationCode);
|
||||||
|
|
||||||
switch (screenRotation) {
|
|
||||||
case Surface.ROTATION_0: degrees = 0; break;
|
|
||||||
case Surface.ROTATION_90: degrees = 90; break;
|
|
||||||
case Surface.ROTATION_180: degrees = 180; break;
|
|
||||||
case Surface.ROTATION_270: degrees = 270; break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Camera.CameraInfo info = new Camera.CameraInfo();
|
Camera.CameraInfo info = new Camera.CameraInfo();
|
||||||
Camera.getCameraInfo(cameraId, info);
|
Camera.getCameraInfo(cameraId, info);
|
||||||
|
|
||||||
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
|
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
|
||||||
return (360 - ((info.orientation + degrees) % 360)) % 360;
|
return (info.orientation + degrees) % 360;
|
||||||
} else {
|
} else {
|
||||||
return (info.orientation - degrees + 360) % 360;
|
return (info.orientation - degrees + 360) % 360;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Comparator<Camera.Size> DESC_SIZE_COMPARATOR = (o1, o2) -> Integer.compare(o2.width * o2.height, o1.width * o1.height);
|
private int getPreviewRotation(int displayRotationCode) {
|
||||||
|
int degrees = convertRotationToDegrees(displayRotationCode);
|
||||||
|
|
||||||
|
Camera.CameraInfo info = new Camera.CameraInfo();
|
||||||
|
Camera.getCameraInfo(cameraId, info);
|
||||||
|
|
||||||
|
int result;
|
||||||
|
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
|
||||||
|
result = (info.orientation + degrees) % 360;
|
||||||
|
result = (360 - result) % 360;
|
||||||
|
} else {
|
||||||
|
result = (info.orientation - degrees + 360) % 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int convertRotationToDegrees(int screenRotation) {
|
||||||
|
switch (screenRotation) {
|
||||||
|
case Surface.ROTATION_0: return 0;
|
||||||
|
case Surface.ROTATION_90: return 90;
|
||||||
|
case Surface.ROTATION_180: return 180;
|
||||||
|
case Surface.ROTATION_270: return 270;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Comparator<Camera.Size> ASC_SIZE_COMPARATOR = (o1, o2) -> Integer.compare(o1.width * o1.height, o2.width * o2.height);
|
||||||
|
|
||||||
private enum Stage {
|
private enum Stage {
|
||||||
INITIALIZED, PREVIEW_STARTED
|
INITIALIZED, PREVIEW_STARTED
|
||||||
@ -194,7 +241,7 @@ public class Camera1Controller {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "cameraCount: " + camera + " previewWidth: " + previewWidth + " previewHeight: " + previewHeight;
|
return "cameraCount: " + cameraCount + " previewWidth: " + previewWidth + " previewHeight: " + previewHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,4 +249,8 @@ public class Camera1Controller {
|
|||||||
void onPropertiesAvailable(@NonNull Properties properties);
|
void onPropertiesAvailable(@NonNull Properties properties);
|
||||||
void onCameraUnavailable();
|
void onCameraUnavailable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CaptureCallback {
|
||||||
|
void onCaptureAvailable(@NonNull byte[] jpegData, boolean frontFacing);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,35 +1,43 @@
|
|||||||
package org.thoughtcrime.securesms.camera;
|
package org.thoughtcrime.securesms.camera;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Matrix;
|
import android.graphics.Matrix;
|
||||||
|
import android.graphics.Point;
|
||||||
import android.graphics.PointF;
|
import android.graphics.PointF;
|
||||||
import android.graphics.SurfaceTexture;
|
import android.graphics.SurfaceTexture;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
import android.hardware.Camera;
|
import android.hardware.Camera;
|
||||||
import android.media.MediaActionSound;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
|
import android.view.Display;
|
||||||
import android.view.GestureDetector;
|
import android.view.GestureDetector;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.TextureView;
|
import android.view.TextureView;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowManager;
|
||||||
import android.view.animation.Animation;
|
import android.view.animation.Animation;
|
||||||
import android.view.animation.AnimationUtils;
|
import android.view.animation.AnimationUtils;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
|
|
||||||
|
import com.bumptech.glide.load.MultiTransformation;
|
||||||
|
import com.bumptech.glide.load.Transformation;
|
||||||
|
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
||||||
|
import com.bumptech.glide.request.target.SimpleTarget;
|
||||||
|
import com.bumptech.glide.request.transition.Transition;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
|
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.LifecycleBoundTask;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|
||||||
@ -59,8 +67,14 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
|
|||||||
throw new IllegalStateException("Parent activity must implement the Controller interface.");
|
throw new IllegalStateException("Parent activity must implement the Controller interface.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WindowManager windowManager = ServiceUtil.getWindowManager(getActivity());
|
||||||
|
Display display = windowManager.getDefaultDisplay();
|
||||||
|
Point displaySize = new Point();
|
||||||
|
|
||||||
|
display.getSize(displaySize);
|
||||||
|
|
||||||
controller = (Controller) getActivity();
|
controller = (Controller) getActivity();
|
||||||
camera = new Camera1Controller(TextSecurePreferences.getDirectCaptureCameraId(getContext()), this);
|
camera = new Camera1Controller(TextSecurePreferences.getDirectCaptureCameraId(getContext()), displaySize.x, displaySize.y, this);
|
||||||
orderEnforcer = new OrderEnforcer<>(Stage.SURFACE_AVAILABLE, Stage.CAMERA_PROPERTIES_AVAILABLE);
|
orderEnforcer = new OrderEnforcer<>(Stage.SURFACE_AVAILABLE, Stage.CAMERA_PROPERTIES_AVAILABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,42 +204,40 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
|
|||||||
private void onCaptureClicked() {
|
private void onCaptureClicked() {
|
||||||
orderEnforcer.reset();
|
orderEnforcer.reset();
|
||||||
|
|
||||||
Stopwatch fastCaptureTimer = new Stopwatch("Fast Capture");
|
Stopwatch fastCaptureTimer = new Stopwatch("Capture");
|
||||||
|
|
||||||
Bitmap preview = cameraPreview.getBitmap();
|
camera.capture((jpegData, frontFacing) -> {
|
||||||
fastCaptureTimer.split("captured");
|
fastCaptureTimer.split("captured");
|
||||||
|
|
||||||
LifecycleBoundTask.run(getLifecycle(), () -> {
|
Transformation<Bitmap> transformation = frontFacing ? new MultiTransformation<>(new CenterCrop(), new FlipTransformation())
|
||||||
Bitmap full = preview;
|
: new CenterCrop();
|
||||||
if (Build.VERSION.SDK_INT < 28) {
|
|
||||||
PointF scale = getScaleTransform(cameraPreview.getWidth(), cameraPreview.getHeight(), properties.getPreviewWidth(), properties.getPreviewHeight());
|
|
||||||
Matrix matrix = new Matrix();
|
|
||||||
|
|
||||||
matrix.setScale(scale.x, scale.y);
|
GlideApp.with(this)
|
||||||
|
.asBitmap()
|
||||||
|
.load(jpegData)
|
||||||
|
.transform(transformation)
|
||||||
|
.override(cameraPreview.getWidth(), cameraPreview.getHeight())
|
||||||
|
.into(new SimpleTarget<Bitmap>() {
|
||||||
|
@Override
|
||||||
|
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
||||||
|
fastCaptureTimer.split("transform");
|
||||||
|
|
||||||
int adjWidth = (int) (cameraPreview.getWidth() / scale.x);
|
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||||
int adjHeight = (int) (cameraPreview.getHeight() / scale.y);
|
resource.compress(Bitmap.CompressFormat.JPEG, 80, stream);
|
||||||
|
fastCaptureTimer.split("compressed");
|
||||||
|
|
||||||
full = Bitmap.createBitmap(preview, 0, 0, adjWidth, adjHeight, matrix, true);
|
byte[] data = stream.toByteArray();
|
||||||
}
|
fastCaptureTimer.split("bytes");
|
||||||
|
fastCaptureTimer.stop(TAG);
|
||||||
|
|
||||||
fastCaptureTimer.split("transformed");
|
controller.onImageCaptured(data);
|
||||||
|
}
|
||||||
|
|
||||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
@Override
|
||||||
full.compress(Bitmap.CompressFormat.JPEG, 80, stream);
|
public void onLoadFailed(@Nullable Drawable errorDrawable) {
|
||||||
fastCaptureTimer.split("compressed");
|
controller.onCameraError();
|
||||||
|
}
|
||||||
byte[] data = stream.toByteArray();
|
});
|
||||||
fastCaptureTimer.split("bytes");
|
|
||||||
fastCaptureTimer.stop(TAG);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}, data -> {
|
|
||||||
if (data != null) {
|
|
||||||
controller.onImageCaptured(data);
|
|
||||||
} else {
|
|
||||||
controller.onCameraError();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,7 +272,11 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
|
|||||||
PointF scale = getScaleTransform(cameraPreview.getWidth(), cameraPreview.getHeight(), properties.getPreviewWidth(), properties.getPreviewHeight());
|
PointF scale = getScaleTransform(cameraPreview.getWidth(), cameraPreview.getHeight(), properties.getPreviewWidth(), properties.getPreviewHeight());
|
||||||
Matrix matrix = new Matrix();
|
Matrix matrix = new Matrix();
|
||||||
|
|
||||||
|
float camWidth = isPortrait() ? Math.min(cameraPreview.getWidth(), cameraPreview.getHeight()) : Math.max(cameraPreview.getWidth(), cameraPreview.getHeight());
|
||||||
|
float camHeight = isPortrait() ? Math.max(cameraPreview.getWidth(), cameraPreview.getHeight()) : Math.min(cameraPreview.getWidth(), cameraPreview.getHeight());
|
||||||
|
|
||||||
matrix.setScale(scale.x, scale.y);
|
matrix.setScale(scale.x, scale.y);
|
||||||
|
matrix.postTranslate((camWidth - (camWidth * scale.x)) / 2, (camHeight - (camHeight * scale.y)) / 2);
|
||||||
cameraPreview.setTransform(matrix);
|
cameraPreview.setTransform(matrix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
package org.thoughtcrime.securesms.camera;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Matrix;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
|
||||||
|
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
|
||||||
|
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
|
||||||
|
public class FlipTransformation extends BitmapTransformation {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) {
|
||||||
|
Bitmap output = pool.get(toTransform.getWidth(), toTransform.getHeight(), toTransform.getConfig());
|
||||||
|
|
||||||
|
Canvas canvas = new Canvas(output);
|
||||||
|
Matrix matrix = new Matrix();
|
||||||
|
matrix.setScale(-1, 1);
|
||||||
|
matrix.postTranslate(toTransform.getWidth(), 0);
|
||||||
|
|
||||||
|
canvas.drawBitmap(toTransform, matrix, null);
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
|
||||||
|
messageDigest.update(FlipTransformation.class.getSimpleName().getBytes());
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user