Add screenshots
- permissions changed to allow access to external storage - button added to interface - code added to build the screenshot and save it to the user's Picture folder
|
@ -9,6 +9,7 @@
|
||||||
package="com.affectiva.affdexme">
|
package="com.affectiva.affdexme">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.camera"
|
android:name="android.hardware.camera"
|
||||||
|
|
|
@ -19,11 +19,13 @@ import android.graphics.Rect;
|
||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
import android.os.Process;
|
import android.os.Process;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
import android.view.SurfaceView;
|
import android.view.SurfaceView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.affectiva.android.affdex.sdk.detector.Face;
|
import com.affectiva.android.affdex.sdk.detector.Face;
|
||||||
|
|
||||||
|
@ -52,6 +54,7 @@ public class DrawingView extends SurfaceView implements SurfaceHolder.Callback {
|
||||||
private SurfaceHolder surfaceHolder;
|
private SurfaceHolder surfaceHolder;
|
||||||
private DrawingThread drawingThread; //DrawingThread object
|
private DrawingThread drawingThread; //DrawingThread object
|
||||||
private DrawingViewConfig drawingViewConfig;
|
private DrawingViewConfig drawingViewConfig;
|
||||||
|
private DrawingThreadEventListener listener;
|
||||||
|
|
||||||
//three constructors required of any custom view
|
//three constructors required of any custom view
|
||||||
public DrawingView(Context context) {
|
public DrawingView(Context context) {
|
||||||
|
@ -73,11 +76,34 @@ public class DrawingView extends SurfaceView implements SurfaceHolder.Callback {
|
||||||
return context.getResources().getIdentifier(name, "drawable", context.getPackageName());
|
return context.getResources().getIdentifier(name, "drawable", context.getPackageName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setEventListener(DrawingThreadEventListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
|
||||||
|
if (drawingThread != null) {
|
||||||
|
drawingThread.setEventListener(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void requestBitmap() {
|
||||||
|
if (listener == null) {
|
||||||
|
String msg = "Attempted to request screenshot without first attaching event listener";
|
||||||
|
Log.e(LOG_TAG, msg);
|
||||||
|
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (drawingThread == null || drawingThread.isStopped()) {
|
||||||
|
String msg = "Attempted to request screenshot without a running drawing thread";
|
||||||
|
Log.e(LOG_TAG, msg);
|
||||||
|
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drawingThread.requestCaptureBitmap = true;
|
||||||
|
}
|
||||||
|
|
||||||
void initView() {
|
void initView() {
|
||||||
surfaceHolder = getHolder(); //The SurfaceHolder object will be used by the thread to request canvas to draw on SurfaceView
|
surfaceHolder = getHolder(); //The SurfaceHolder object will be used by the thread to request canvas to draw on SurfaceView
|
||||||
surfaceHolder.setFormat(PixelFormat.TRANSPARENT); //set to Transparent so this surfaceView does not obscure the one it is overlaying (the one displaying the camera).
|
surfaceHolder.setFormat(PixelFormat.TRANSPARENT); //set to Transparent so this surfaceView does not obscure the one it is overlaying (the one displaying the camera).
|
||||||
surfaceHolder.addCallback(this); //become a Listener to the three events below that SurfaceView generates
|
surfaceHolder.addCallback(this); //become a Listener to the three events below that SurfaceView generates
|
||||||
|
|
||||||
drawingViewConfig = new DrawingViewConfig();
|
drawingViewConfig = new DrawingViewConfig();
|
||||||
|
|
||||||
//Default values
|
//Default values
|
||||||
|
@ -133,7 +159,7 @@ public class DrawingView extends SurfaceView implements SurfaceHolder.Callback {
|
||||||
|
|
||||||
drawingViewConfig.setDominantEmotionLabelPaints(emotionLabelPaint, emotionValuePaint);
|
drawingViewConfig.setDominantEmotionLabelPaints(emotionLabelPaint, emotionValuePaint);
|
||||||
drawingViewConfig.setDominantEmotionMetricBarConfig(metricBarPaint, metricBarWidth);
|
drawingViewConfig.setDominantEmotionMetricBarConfig(metricBarPaint, metricBarWidth);
|
||||||
drawingThread = new DrawingThread(surfaceHolder, drawingViewConfig);
|
drawingThread = new DrawingThread(surfaceHolder, drawingViewConfig, listener);
|
||||||
|
|
||||||
//statically load the emoji bitmaps on-demand and cache
|
//statically load the emoji bitmaps on-demand and cache
|
||||||
emojiMarkerBitmapToEmojiTypeMap = new HashMap<>();
|
emojiMarkerBitmapToEmojiTypeMap = new HashMap<>();
|
||||||
|
@ -147,7 +173,7 @@ public class DrawingView extends SurfaceView implements SurfaceHolder.Callback {
|
||||||
@Override
|
@Override
|
||||||
public void surfaceCreated(SurfaceHolder holder) {
|
public void surfaceCreated(SurfaceHolder holder) {
|
||||||
if (drawingThread.isStopped()) {
|
if (drawingThread.isStopped()) {
|
||||||
drawingThread = new DrawingThread(surfaceHolder, drawingViewConfig);
|
drawingThread = new DrawingThread(surfaceHolder, drawingViewConfig, listener);
|
||||||
}
|
}
|
||||||
drawingThread.start();
|
drawingThread.start();
|
||||||
}
|
}
|
||||||
|
@ -261,6 +287,10 @@ public class DrawingView extends SurfaceView implements SurfaceHolder.Callback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DrawingThreadEventListener {
|
||||||
|
void onBitmapGenerated(Bitmap bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
class FacesSharer {
|
class FacesSharer {
|
||||||
boolean isPointsMirrored;
|
boolean isPointsMirrored;
|
||||||
List<Face> facesToDraw;
|
List<Face> facesToDraw;
|
||||||
|
@ -279,9 +309,11 @@ public class DrawingView extends SurfaceView implements SurfaceHolder.Callback {
|
||||||
private Paint boundingBoxPaint;
|
private Paint boundingBoxPaint;
|
||||||
private Paint dominantEmotionScoreBarPaint;
|
private Paint dominantEmotionScoreBarPaint;
|
||||||
private volatile boolean stopFlag = false; //boolean to indicate when thread has been told to stop
|
private volatile boolean stopFlag = false; //boolean to indicate when thread has been told to stop
|
||||||
|
private volatile boolean requestCaptureBitmap = false; //boolean to indicate a snapshot of the surface has been requested
|
||||||
private DrawingViewConfig config;
|
private DrawingViewConfig config;
|
||||||
|
private DrawingThreadEventListener listener;
|
||||||
|
|
||||||
public DrawingThread(SurfaceHolder surfaceHolder, DrawingViewConfig con) {
|
public DrawingThread(SurfaceHolder surfaceHolder, DrawingViewConfig con, DrawingThreadEventListener listener) {
|
||||||
mSurfaceHolder = surfaceHolder;
|
mSurfaceHolder = surfaceHolder;
|
||||||
|
|
||||||
//statically load the Appearance marker bitmaps so they only have to load once
|
//statically load the Appearance marker bitmaps so they only have to load once
|
||||||
|
@ -303,10 +335,15 @@ public class DrawingView extends SurfaceView implements SurfaceHolder.Callback {
|
||||||
|
|
||||||
config = con;
|
config = con;
|
||||||
sharer = new FacesSharer();
|
sharer = new FacesSharer();
|
||||||
|
this.listener = listener;
|
||||||
|
|
||||||
setThickness(config.drawThickness);
|
setThickness(config.drawThickness);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setEventListener(DrawingThreadEventListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
void setValenceOfBoundingBox(float valence) {
|
void setValenceOfBoundingBox(float valence) {
|
||||||
//prepare the color of the bounding box using the valence score. Red for -100, White for 0, and Green for +100, with linear interpolation in between.
|
//prepare the color of the bounding box using the valence score. Red for -100, White for 0, and Green for +100, with linear interpolation in between.
|
||||||
if (valence > 0) {
|
if (valence > 0) {
|
||||||
|
@ -359,26 +396,40 @@ public class DrawingView extends SurfaceView implements SurfaceHolder.Callback {
|
||||||
* After we are done drawing, we let go of the canvas using SurfaceHolder.unlockCanvasAndPost()
|
* After we are done drawing, we let go of the canvas using SurfaceHolder.unlockCanvasAndPost()
|
||||||
* **/
|
* **/
|
||||||
Canvas c = null;
|
Canvas c = null;
|
||||||
|
Canvas screenshotCanvas = null;
|
||||||
|
Bitmap screenshotBitmap = null;
|
||||||
try {
|
try {
|
||||||
c = mSurfaceHolder.lockCanvas();
|
c = mSurfaceHolder.lockCanvas();
|
||||||
|
|
||||||
|
if (requestCaptureBitmap) {
|
||||||
|
Rect surfaceBounds = mSurfaceHolder.getSurfaceFrame();
|
||||||
|
screenshotBitmap = Bitmap.createBitmap(surfaceBounds.width(), surfaceBounds.height(), Bitmap.Config.ARGB_8888);
|
||||||
|
screenshotCanvas = new Canvas(screenshotBitmap);
|
||||||
|
requestCaptureBitmap = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (c != null) {
|
if (c != null) {
|
||||||
synchronized (mSurfaceHolder) {
|
synchronized (mSurfaceHolder) {
|
||||||
c.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); //clear previous dots
|
c.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); //clear previous dots
|
||||||
draw(c);
|
draw(c, screenshotCanvas);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
if (c != null) {
|
if (c != null) {
|
||||||
mSurfaceHolder.unlockCanvasAndPost(c);
|
mSurfaceHolder.unlockCanvasAndPost(c);
|
||||||
}
|
}
|
||||||
|
if (screenshotBitmap != null && listener != null) {
|
||||||
|
listener.onBitmapGenerated(Bitmap.createBitmap(screenshotBitmap));
|
||||||
|
screenshotBitmap.recycle();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config = null; //nullify object to avoid memory leak
|
config = null; //nullify object to avoid memory leak
|
||||||
}
|
}
|
||||||
|
|
||||||
void draw(Canvas c) {
|
void draw(@NonNull Canvas c, @Nullable Canvas c2) {
|
||||||
Face nextFaceToDraw;
|
Face nextFaceToDraw;
|
||||||
boolean mirrorPoints;
|
boolean mirrorPoints;
|
||||||
boolean multiFaceMode;
|
boolean multiFaceMode;
|
||||||
|
@ -400,6 +451,10 @@ public class DrawingView extends SurfaceView implements SurfaceHolder.Callback {
|
||||||
|
|
||||||
drawFaceAttributes(c, nextFaceToDraw, mirrorPoints, multiFaceMode);
|
drawFaceAttributes(c, nextFaceToDraw, mirrorPoints, multiFaceMode);
|
||||||
|
|
||||||
|
if (c2 != null) {
|
||||||
|
drawFaceAttributes(c2, nextFaceToDraw, false, multiFaceMode);
|
||||||
|
}
|
||||||
|
|
||||||
synchronized (sharer) {
|
synchronized (sharer) {
|
||||||
mirrorPoints = sharer.isPointsMirrored;
|
mirrorPoints = sharer.isPointsMirrored;
|
||||||
|
|
||||||
|
|
|
@ -5,22 +5,31 @@
|
||||||
|
|
||||||
package com.affectiva.affdexme;
|
package com.affectiva.affdexme;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.graphics.ImageFormat;
|
||||||
import android.graphics.Matrix;
|
import android.graphics.Matrix;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.YuvImage;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.provider.MediaStore;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import com.affectiva.android.affdex.sdk.Frame;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
public class ImageHelper {
|
public class ImageHelper {
|
||||||
|
|
||||||
|
@ -30,7 +39,7 @@ public class ImageHelper {
|
||||||
private ImageHelper() {
|
private ImageHelper() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean checkIfImageFileExists(@NonNull Context context, @NonNull String fileName) {
|
public static boolean checkIfImageFileExists(@NonNull final Context context, @NonNull final String fileName) {
|
||||||
|
|
||||||
// path to /data/data/yourapp/app_data/images
|
// path to /data/data/yourapp/app_data/images
|
||||||
File directory = context.getDir("images", Context.MODE_PRIVATE);
|
File directory = context.getDir("images", Context.MODE_PRIVATE);
|
||||||
|
@ -41,7 +50,7 @@ public class ImageHelper {
|
||||||
return imagePath.exists();
|
return imagePath.exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean deleteImageFile(@NonNull Context context, @NonNull String fileName) {
|
public static boolean deleteImageFile(@NonNull final Context context, @NonNull final String fileName) {
|
||||||
// path to /data/data/yourapp/app_data/images
|
// path to /data/data/yourapp/app_data/images
|
||||||
File directory = context.getDir("images", Context.MODE_PRIVATE);
|
File directory = context.getDir("images", Context.MODE_PRIVATE);
|
||||||
|
|
||||||
|
@ -51,7 +60,7 @@ public class ImageHelper {
|
||||||
return imagePath.delete();
|
return imagePath.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void resizeAndSaveResourceImageToInternalStorage(@NonNull Context context, @NonNull String fileName, @NonNull String resourceName) throws FileNotFoundException {
|
public static void resizeAndSaveResourceImageToInternalStorage(@NonNull final Context context, @NonNull final String fileName, @NonNull final String resourceName) throws FileNotFoundException {
|
||||||
final int resourceId = context.getResources().getIdentifier(resourceName, "drawable", context.getPackageName());
|
final int resourceId = context.getResources().getIdentifier(resourceName, "drawable", context.getPackageName());
|
||||||
|
|
||||||
if (resourceId == 0) {
|
if (resourceId == 0) {
|
||||||
|
@ -61,7 +70,7 @@ public class ImageHelper {
|
||||||
resizeAndSaveResourceImageToInternalStorage(context, fileName, resourceId);
|
resizeAndSaveResourceImageToInternalStorage(context, fileName, resourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void resizeAndSaveResourceImageToInternalStorage(@NonNull Context context, @NonNull String fileName, int resourceId) {
|
public static void resizeAndSaveResourceImageToInternalStorage(@NonNull final Context context, @NonNull final String fileName, final int resourceId) {
|
||||||
Resources resources = context.getResources();
|
Resources resources = context.getResources();
|
||||||
Bitmap sourceBitmap = BitmapFactory.decodeResource(resources, resourceId);
|
Bitmap sourceBitmap = BitmapFactory.decodeResource(resources, resourceId);
|
||||||
Bitmap resizedBitmap = resizeBitmapForDeviceDensity(context, sourceBitmap);
|
Bitmap resizedBitmap = resizeBitmapForDeviceDensity(context, sourceBitmap);
|
||||||
|
@ -70,7 +79,7 @@ public class ImageHelper {
|
||||||
resizedBitmap.recycle();
|
resizedBitmap.recycle();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Bitmap resizeBitmapForDeviceDensity(@NonNull Context context, @NonNull Bitmap sourceBitmap) {
|
public static Bitmap resizeBitmapForDeviceDensity(@NonNull final Context context, @NonNull final Bitmap sourceBitmap) {
|
||||||
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
||||||
|
|
||||||
int targetWidth = Math.round(sourceBitmap.getWidth() * metrics.density);
|
int targetWidth = Math.round(sourceBitmap.getWidth() * metrics.density);
|
||||||
|
@ -79,7 +88,7 @@ public class ImageHelper {
|
||||||
return Bitmap.createScaledBitmap(sourceBitmap, targetWidth, targetHeight, false);
|
return Bitmap.createScaledBitmap(sourceBitmap, targetWidth, targetHeight, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void saveBitmapToInternalStorage(@NonNull Context context, @NonNull Bitmap bitmapImage, @NonNull String fileName) {
|
public static void saveBitmapToInternalStorage(@NonNull final Context context, @NonNull final Bitmap bitmapImage, @NonNull final String fileName) {
|
||||||
|
|
||||||
// path to /data/data/yourapp/app_data/images
|
// path to /data/data/yourapp/app_data/images
|
||||||
File directory = context.getDir("images", Context.MODE_PRIVATE);
|
File directory = context.getDir("images", Context.MODE_PRIVATE);
|
||||||
|
@ -109,7 +118,7 @@ public class ImageHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Bitmap loadBitmapFromInternalStorage(@NonNull Context applicationContext, @NonNull String fileName) {
|
public static Bitmap loadBitmapFromInternalStorage(@NonNull final Context applicationContext, @NonNull final String fileName) {
|
||||||
|
|
||||||
// path to /data/data/yourapp/app_data/images
|
// path to /data/data/yourapp/app_data/images
|
||||||
File directory = applicationContext.getDir("images", Context.MODE_PRIVATE);
|
File directory = applicationContext.getDir("images", Context.MODE_PRIVATE);
|
||||||
|
@ -125,7 +134,7 @@ public class ImageHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void preproccessImageIfNecessary(Context context, String fileName, String resourceName) {
|
public static void preproccessImageIfNecessary(@NonNull final Context context, @NonNull final String fileName, @NonNull final String resourceName) {
|
||||||
// Set this to true to force the app to always load the images for debugging purposes
|
// Set this to true to force the app to always load the images for debugging purposes
|
||||||
final boolean DEBUG = false;
|
final boolean DEBUG = false;
|
||||||
|
|
||||||
|
@ -157,10 +166,10 @@ public class ImageHelper {
|
||||||
* @param imageView source ImageView
|
* @param imageView source ImageView
|
||||||
* @return 0: left, 1: top, 2: width, 3: height
|
* @return 0: left, 1: top, 2: width, 3: height
|
||||||
*/
|
*/
|
||||||
public static int[] getBitmapPositionInsideImageView(ImageView imageView) {
|
public static int[] getBitmapPositionInsideImageView(@NonNull final ImageView imageView) {
|
||||||
int[] ret = new int[4];
|
int[] ret = new int[4];
|
||||||
|
|
||||||
if (imageView == null || imageView.getDrawable() == null)
|
if (imageView.getDrawable() == null)
|
||||||
return ret;
|
return ret;
|
||||||
|
|
||||||
// Get image dimensions
|
// Get image dimensions
|
||||||
|
@ -197,4 +206,101 @@ public class ImageHelper {
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a HACK.
|
||||||
|
* We need to update the Android SDK to make this process cleaner.
|
||||||
|
* We should just be able to call frame.getBitmap() and have it return a bitmap no matter what type
|
||||||
|
* of frame it is. If any conversion between file types needs to take place, it needs to happen
|
||||||
|
* inside the SDK layer and put the onus on the developer to know how to convert between YUV and ARGB.
|
||||||
|
* TODO: See above
|
||||||
|
*
|
||||||
|
* @param frame - The Frame containing the desired image
|
||||||
|
* @return - The Bitmap representation of the image
|
||||||
|
*/
|
||||||
|
public static Bitmap getBitmapFromFrame(@NonNull final Frame frame) {
|
||||||
|
Bitmap bitmap;
|
||||||
|
|
||||||
|
if (frame instanceof Frame.BitmapFrame) {
|
||||||
|
bitmap = ((Frame.BitmapFrame) frame).getBitmap();
|
||||||
|
} else { //frame is ByteArrayFrame
|
||||||
|
switch (frame.getColorFormat()) {
|
||||||
|
case RGBA:
|
||||||
|
bitmap = getBitmapFromRGBFrame(frame);
|
||||||
|
break;
|
||||||
|
case YUV_NV21:
|
||||||
|
bitmap = getBitmapFromYuvFrame(frame);
|
||||||
|
break;
|
||||||
|
case UNKNOWN_TYPE:
|
||||||
|
default:
|
||||||
|
Log.e(LOG_TAG, "Unable to get bitmap from unknown frame type");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bitmap == null || frame.getTargetRotation().toDouble() == 0.0) {
|
||||||
|
return bitmap;
|
||||||
|
} else {
|
||||||
|
return rotateBitmap(bitmap, (float) frame.getTargetRotation().toDouble());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Bitmap getBitmapFromRGBFrame(@NonNull final Frame frame) {
|
||||||
|
byte[] pixels = ((Frame.ByteArrayFrame) frame).getByteArray();
|
||||||
|
Bitmap bitmap = Bitmap.createBitmap(frame.getWidth(), frame.getHeight(), Bitmap.Config.ARGB_8888);
|
||||||
|
bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(pixels));
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Bitmap getBitmapFromYuvFrame(@NonNull final Frame frame) {
|
||||||
|
byte[] pixels = ((Frame.ByteArrayFrame) frame).getByteArray();
|
||||||
|
YuvImage yuvImage = new YuvImage(pixels, ImageFormat.NV21, frame.getWidth(), frame.getHeight(), null);
|
||||||
|
return convertYuvImageToBitmap(yuvImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note: This conversion procedure is sloppy and may result in JPEG compression artifacts
|
||||||
|
*
|
||||||
|
* @param yuvImage - The YuvImage to convert
|
||||||
|
* @return - The converted Bitmap
|
||||||
|
*/
|
||||||
|
public static Bitmap convertYuvImageToBitmap(@NonNull final YuvImage yuvImage) {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
yuvImage.compressToJpeg(new Rect(0, 0, yuvImage.getWidth(), yuvImage.getHeight()), 100, out);
|
||||||
|
byte[] imageBytes = out.toByteArray();
|
||||||
|
try {
|
||||||
|
out.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(LOG_TAG, "Exception while closing output stream", e);
|
||||||
|
}
|
||||||
|
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Bitmap rotateBitmap(@NonNull final Bitmap source, final float angle) {
|
||||||
|
Matrix matrix = new Matrix();
|
||||||
|
matrix.postRotate(angle);
|
||||||
|
return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void saveBitmapToFileAsPng(@NonNull final Bitmap bitmap, @NonNull final File file) throws IOException {
|
||||||
|
try {
|
||||||
|
FileOutputStream outputStream = new FileOutputStream(file);
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
|
||||||
|
bitmap.recycle();
|
||||||
|
outputStream.flush();
|
||||||
|
outputStream.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FileNotFoundException("Unable to save bitmap to file: " + file.getPath() + "\n" + e.getLocalizedMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void addPngToGallery(@NonNull final Context context, @NonNull final File imageFile) {
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
|
||||||
|
values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
|
||||||
|
values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
|
||||||
|
values.put(MediaStore.MediaColumns.DATA, imageFile.getAbsolutePath());
|
||||||
|
|
||||||
|
context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,15 +11,21 @@ import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Rect;
|
||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Environment;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.v4.app.ActivityCompat;
|
import android.support.v4.app.ActivityCompat;
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
import android.support.v7.app.AppCompatActivity;
|
||||||
|
import android.text.format.DateFormat;
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
|
@ -42,9 +48,11 @@ import com.affectiva.android.affdex.sdk.detector.CameraDetector;
|
||||||
import com.affectiva.android.affdex.sdk.detector.Detector;
|
import com.affectiva.android.affdex.sdk.detector.Detector;
|
||||||
import com.affectiva.android.affdex.sdk.detector.Face;
|
import com.affectiva.android.affdex.sdk.detector.Face;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
@ -80,17 +88,20 @@ import java.util.Locale;
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity
|
public class MainActivity extends AppCompatActivity
|
||||||
implements Detector.FaceListener, Detector.ImageListener, CameraDetector.CameraEventListener,
|
implements Detector.FaceListener, Detector.ImageListener, CameraDetector.CameraEventListener,
|
||||||
View.OnTouchListener, ActivityCompat.OnRequestPermissionsResultCallback {
|
View.OnTouchListener, ActivityCompat.OnRequestPermissionsResultCallback, DrawingView.DrawingThreadEventListener {
|
||||||
|
|
||||||
public static final int MAX_SUPPORTED_FACES = 4;
|
public static final int MAX_SUPPORTED_FACES = 3;
|
||||||
|
public static final boolean STORE_RAW_SCREENSHOTS = false; // setting to enable saving the raw images when taking screenshots
|
||||||
public static final int NUM_METRICS_DISPLAYED = 6;
|
public static final int NUM_METRICS_DISPLAYED = 6;
|
||||||
private static final String LOG_TAG = "AffdexMe";
|
private static final String LOG_TAG = "AffdexMe";
|
||||||
private static final int AFFDEXME_PERMISSIONS_REQUEST = 42; //value is arbitrary (between 0 and 255)
|
private static final int CAMERA_PERMISSIONS_REQUEST = 42; //value is arbitrary (between 0 and 255)
|
||||||
|
private static final int EXTERNAL_STORAGE_PERMISSIONS_REQUEST = 73;
|
||||||
int cameraPreviewWidth = 0;
|
int cameraPreviewWidth = 0;
|
||||||
int cameraPreviewHeight = 0;
|
int cameraPreviewHeight = 0;
|
||||||
CameraDetector.CameraType cameraType;
|
CameraDetector.CameraType cameraType;
|
||||||
boolean mirrorPoints = false;
|
boolean mirrorPoints = false;
|
||||||
private boolean cameraPermissionsAvailable = false;
|
private boolean cameraPermissionsAvailable = false;
|
||||||
|
private boolean storagePermissionsAvailable = false;
|
||||||
private CameraDetector detector = null;
|
private CameraDetector detector = null;
|
||||||
private RelativeLayout metricViewLayout;
|
private RelativeLayout metricViewLayout;
|
||||||
private LinearLayout leftMetricsLayout;
|
private LinearLayout leftMetricsLayout;
|
||||||
|
@ -108,6 +119,8 @@ public class MainActivity extends AppCompatActivity
|
||||||
private DrawingView drawingView; //SurfaceView containing its own thread, used to draw facial tracking dots
|
private DrawingView drawingView; //SurfaceView containing its own thread, used to draw facial tracking dots
|
||||||
private ImageButton settingsButton;
|
private ImageButton settingsButton;
|
||||||
private ImageButton cameraButton;
|
private ImageButton cameraButton;
|
||||||
|
private ImageButton screenshotButton;
|
||||||
|
private Frame mostRecentFrame;
|
||||||
private boolean isMenuVisible = false;
|
private boolean isMenuVisible = false;
|
||||||
private boolean isFPSVisible = false;
|
private boolean isFPSVisible = false;
|
||||||
private boolean isMenuShowingForFirstTime = true;
|
private boolean isMenuShowingForFirstTime = true;
|
||||||
|
@ -125,7 +138,7 @@ public class MainActivity extends AppCompatActivity
|
||||||
preproccessMetricImages();
|
preproccessMetricImages();
|
||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
initializeUI();
|
initializeUI();
|
||||||
checkForDangerousPermissions();
|
checkForCameraPermissions();
|
||||||
determineCameraAvailability();
|
determineCameraAvailability();
|
||||||
initializeCameraDetector();
|
initializeCameraDetector();
|
||||||
}
|
}
|
||||||
|
@ -153,8 +166,7 @@ public class MainActivity extends AppCompatActivity
|
||||||
ImageHelper.preproccessImageIfNecessary(context, "unknown_noglasses.png", "unknown_noglasses");
|
ImageHelper.preproccessImageIfNecessary(context, "unknown_noglasses.png", "unknown_noglasses");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void checkForCameraPermissions() {
|
||||||
private void checkForDangerousPermissions() {
|
|
||||||
cameraPermissionsAvailable =
|
cameraPermissionsAvailable =
|
||||||
ContextCompat.checkSelfPermission(
|
ContextCompat.checkSelfPermission(
|
||||||
getApplicationContext(),
|
getApplicationContext(),
|
||||||
|
@ -168,35 +180,67 @@ public class MainActivity extends AppCompatActivity
|
||||||
// Show an explanation to the user *asynchronously* -- don't block
|
// Show an explanation to the user *asynchronously* -- don't block
|
||||||
// this thread waiting for the user's response! After the user
|
// this thread waiting for the user's response! After the user
|
||||||
// sees the explanation, try again to request the permission.
|
// sees the explanation, try again to request the permission.
|
||||||
showPermissionExplanationDialog();
|
showPermissionExplanationDialog(CAMERA_PERMISSIONS_REQUEST);
|
||||||
} else {
|
} else {
|
||||||
// No explanation needed, we can request the permission.
|
// No explanation needed, we can request the permission.
|
||||||
requestNeededPermissions();
|
requestCameraPermissions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requestNeededPermissions() {
|
private void checkForStoragePermissions() {
|
||||||
List<String> neededPermissions = new ArrayList<>();
|
storagePermissionsAvailable =
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
|
getApplicationContext(),
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
|
||||||
|
|
||||||
if (!cameraPermissionsAvailable) {
|
if (!storagePermissionsAvailable) {
|
||||||
neededPermissions.add(Manifest.permission.CAMERA);
|
|
||||||
|
// Should we show an explanation?
|
||||||
|
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||||
|
|
||||||
|
// Show an explanation to the user *asynchronously* -- don't block
|
||||||
|
// this thread waiting for the user's response! After the user
|
||||||
|
// sees the explanation, try again to request the permission.
|
||||||
|
showPermissionExplanationDialog(EXTERNAL_STORAGE_PERMISSIONS_REQUEST);
|
||||||
|
} else {
|
||||||
|
// No explanation needed, we can request the permission.
|
||||||
|
requestStoragePermissions();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
takeScreenshot(screenshotButton);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ActivityCompat.requestPermissions(
|
private void requestStoragePermissions() {
|
||||||
this,
|
if (!storagePermissionsAvailable) {
|
||||||
neededPermissions.toArray(new String[neededPermissions.size()]),
|
ActivityCompat.requestPermissions(
|
||||||
AFFDEXME_PERMISSIONS_REQUEST);
|
this,
|
||||||
|
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||||
|
EXTERNAL_STORAGE_PERMISSIONS_REQUEST);
|
||||||
|
|
||||||
// AFFDEXME_PERMISSIONS_REQUEST is an app-defined int constant that must be between 0 and 255.
|
// EXTERNAL_STORAGE_PERMISSIONS_REQUEST is an app-defined int constant that must be between 0 and 255.
|
||||||
// The callback method gets the result of the request.
|
// The callback method gets the result of the request.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestCameraPermissions() {
|
||||||
|
if (!cameraPermissionsAvailable) {
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
this,
|
||||||
|
new String[]{Manifest.permission.CAMERA},
|
||||||
|
CAMERA_PERMISSIONS_REQUEST);
|
||||||
|
|
||||||
|
// CAMERA_PERMISSIONS_REQUEST is an app-defined int constant that must be between 0 and 255.
|
||||||
|
// The callback method gets the result of the request.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
|
|
||||||
if (requestCode == AFFDEXME_PERMISSIONS_REQUEST) {
|
if (requestCode == CAMERA_PERMISSIONS_REQUEST) {
|
||||||
for (int i = 0; i < permissions.length; i++) {
|
for (int i = 0; i < permissions.length; i++) {
|
||||||
String permission = permissions[i];
|
String permission = permissions[i];
|
||||||
int grantResult = grantResults[i];
|
int grantResult = grantResults[i];
|
||||||
|
@ -205,32 +249,61 @@ public class MainActivity extends AppCompatActivity
|
||||||
cameraPermissionsAvailable = (grantResult == PackageManager.PERMISSION_GRANTED);
|
cameraPermissionsAvailable = (grantResult == PackageManager.PERMISSION_GRANTED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!cameraPermissionsAvailable) {
|
||||||
|
permissionsUnavailableLayout.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
permissionsUnavailableLayout.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cameraPermissionsAvailable) {
|
if (requestCode == EXTERNAL_STORAGE_PERMISSIONS_REQUEST) {
|
||||||
permissionsUnavailableLayout.setVisibility(View.VISIBLE);
|
for (int i = 0; i < permissions.length; i++) {
|
||||||
} else {
|
String permission = permissions[i];
|
||||||
permissionsUnavailableLayout.setVisibility(View.GONE);
|
int grantResult = grantResults[i];
|
||||||
|
|
||||||
|
if (permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||||
|
storagePermissionsAvailable = (grantResult == PackageManager.PERMISSION_GRANTED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storagePermissionsAvailable) {
|
||||||
|
// resume taking the screenshot
|
||||||
|
takeScreenshot(screenshotButton);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showPermissionExplanationDialog() {
|
private void showPermissionExplanationDialog(int requestCode) {
|
||||||
final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(
|
final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(
|
||||||
getApplicationContext());
|
MainActivity.this);
|
||||||
|
|
||||||
// set title
|
// set title
|
||||||
alertDialogBuilder.setTitle(getResources().getString(R.string.insufficient_permissions));
|
alertDialogBuilder.setTitle(getResources().getString(R.string.insufficient_permissions));
|
||||||
|
|
||||||
// set dialog message
|
// set dialog message
|
||||||
alertDialogBuilder
|
if (requestCode == CAMERA_PERMISSIONS_REQUEST) {
|
||||||
.setMessage(getResources().getString(R.string.permissions_needed_explanation))
|
alertDialogBuilder
|
||||||
.setCancelable(false)
|
.setMessage(getResources().getString(R.string.permissions_camera_needed_explanation))
|
||||||
.setPositiveButton(getResources().getString(R.string.understood), new DialogInterface.OnClickListener() {
|
.setCancelable(false)
|
||||||
public void onClick(DialogInterface dialog, int id) {
|
.setPositiveButton(getResources().getString(R.string.understood), new DialogInterface.OnClickListener() {
|
||||||
dialog.cancel();
|
public void onClick(DialogInterface dialog, int id) {
|
||||||
requestNeededPermissions();
|
dialog.cancel();
|
||||||
}
|
requestCameraPermissions();
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
} else if (requestCode == EXTERNAL_STORAGE_PERMISSIONS_REQUEST) {
|
||||||
|
alertDialogBuilder
|
||||||
|
.setMessage(getResources().getString(R.string.permissions_storage_needed_explanation))
|
||||||
|
.setCancelable(false)
|
||||||
|
.setPositiveButton(getResources().getString(R.string.understood), new DialogInterface.OnClickListener() {
|
||||||
|
public void onClick(DialogInterface dialog, int id) {
|
||||||
|
dialog.cancel();
|
||||||
|
requestStoragePermissions();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// create alert dialog
|
// create alert dialog
|
||||||
AlertDialog alertDialog = alertDialogBuilder.create();
|
AlertDialog alertDialog = alertDialogBuilder.create();
|
||||||
|
@ -239,7 +312,6 @@ public class MainActivity extends AppCompatActivity
|
||||||
alertDialog.show();
|
alertDialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We check to make sure the device has a front-facing camera.
|
* We check to make sure the device has a front-facing camera.
|
||||||
* If it does not, we obscure the app with a notice informing the user they cannot
|
* If it does not, we obscure the app with a notice informing the user they cannot
|
||||||
|
@ -287,6 +359,7 @@ public class MainActivity extends AppCompatActivity
|
||||||
drawingView = (DrawingView) findViewById(R.id.drawing_view);
|
drawingView = (DrawingView) findViewById(R.id.drawing_view);
|
||||||
settingsButton = (ImageButton) findViewById(R.id.settings_button);
|
settingsButton = (ImageButton) findViewById(R.id.settings_button);
|
||||||
cameraButton = (ImageButton) findViewById(R.id.camera_button);
|
cameraButton = (ImageButton) findViewById(R.id.camera_button);
|
||||||
|
screenshotButton = (ImageButton) findViewById(R.id.screenshot_button);
|
||||||
progressBar = (ProgressBar) findViewById(R.id.progress_bar);
|
progressBar = (ProgressBar) findViewById(R.id.progress_bar);
|
||||||
pleaseWaitTextView = (TextView) findViewById(R.id.please_wait_textview);
|
pleaseWaitTextView = (TextView) findViewById(R.id.please_wait_textview);
|
||||||
Button retryPermissionsButton = (Button) findViewById(R.id.retryPermissionsButton);
|
Button retryPermissionsButton = (Button) findViewById(R.id.retryPermissionsButton);
|
||||||
|
@ -336,6 +409,9 @@ public class MainActivity extends AppCompatActivity
|
||||||
//Attach event listeners to the menu and edit box
|
//Attach event listeners to the menu and edit box
|
||||||
activityLayout.setOnTouchListener(this);
|
activityLayout.setOnTouchListener(this);
|
||||||
|
|
||||||
|
//Attach event listerner to drawing view
|
||||||
|
drawingView.setEventListener(this);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This app sets the View.SYSTEM_UI_FLAG_HIDE_NAVIGATION flag. Unfortunately, this flag causes
|
* This app sets the View.SYSTEM_UI_FLAG_HIDE_NAVIGATION flag. Unfortunately, this flag causes
|
||||||
* Android to steal the first touch event after the navigation bar has been hidden, a touch event
|
* Android to steal the first touch event after the navigation bar has been hidden, a touch event
|
||||||
|
@ -356,7 +432,7 @@ public class MainActivity extends AppCompatActivity
|
||||||
retryPermissionsButton.setOnClickListener(new View.OnClickListener() {
|
retryPermissionsButton.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
requestNeededPermissions();
|
requestCameraPermissions();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -381,7 +457,7 @@ public class MainActivity extends AppCompatActivity
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
checkForDangerousPermissions();
|
checkForCameraPermissions();
|
||||||
restoreApplicationSettings();
|
restoreApplicationSettings();
|
||||||
setMenuVisible(true);
|
setMenuVisible(true);
|
||||||
isMenuShowingForFirstTime = true;
|
isMenuShowingForFirstTime = true;
|
||||||
|
@ -576,7 +652,6 @@ public class MainActivity extends AppCompatActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFaceDetectionStarted() {
|
public void onFaceDetectionStarted() {
|
||||||
leftMetricsLayout.animate().alpha(1); //make left and right metrics appear
|
leftMetricsLayout.animate().alpha(1); //make left and right metrics appear
|
||||||
|
@ -601,6 +676,8 @@ public class MainActivity extends AppCompatActivity
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onImageResults(List<Face> faces, Frame image, float timeStamp) {
|
public void onImageResults(List<Face> faces, Frame image, float timeStamp) {
|
||||||
|
mostRecentFrame = image;
|
||||||
|
|
||||||
//If the faces object is null, we received an unprocessed frame
|
//If the faces object is null, we received an unprocessed frame
|
||||||
if (faces == null) {
|
if (faces == null) {
|
||||||
return;
|
return;
|
||||||
|
@ -638,6 +715,104 @@ public class MainActivity extends AppCompatActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void takeScreenshot(View view) {
|
||||||
|
// Check the permissions to see if we are allowed to save the screenshot
|
||||||
|
if (!storagePermissionsAvailable) {
|
||||||
|
checkForStoragePermissions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawingView.requestBitmap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A screenshot of the drawing view is generated and processing continues via the callback
|
||||||
|
* onBitmapGenerated() which calls processScreenshot().
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processScreenshot(Bitmap drawingViewBitmap, boolean alsoSaveRaw) {
|
||||||
|
if (mostRecentFrame == null) {
|
||||||
|
Toast.makeText(getApplicationContext(), "No frame detected, aborting screenshot", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!storagePermissionsAvailable) {
|
||||||
|
checkForStoragePermissions();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bitmap faceBitmap = ImageHelper.getBitmapFromFrame(mostRecentFrame);
|
||||||
|
|
||||||
|
if (faceBitmap == null) {
|
||||||
|
Log.e(LOG_TAG, "Unable to generate bitmap for frame, aborting screenshot");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
metricViewLayout.setDrawingCacheEnabled(true);
|
||||||
|
Bitmap metricsBitmap = Bitmap.createBitmap(metricViewLayout.getDrawingCache());
|
||||||
|
metricViewLayout.setDrawingCacheEnabled(false);
|
||||||
|
|
||||||
|
Bitmap finalScreenshot = Bitmap.createBitmap(faceBitmap.getWidth(), faceBitmap.getHeight(), Bitmap.Config.ARGB_8888);
|
||||||
|
Canvas canvas = new Canvas(finalScreenshot);
|
||||||
|
Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
|
||||||
|
|
||||||
|
canvas.drawBitmap(faceBitmap, 0, 0, paint);
|
||||||
|
|
||||||
|
float scaleFactor = ((float) faceBitmap.getWidth()) / ((float) drawingViewBitmap.getWidth());
|
||||||
|
int scaledHeight = Math.round(drawingViewBitmap.getHeight() * scaleFactor);
|
||||||
|
canvas.drawBitmap(drawingViewBitmap, null, new Rect(0, 0, faceBitmap.getWidth(), scaledHeight), paint);
|
||||||
|
|
||||||
|
scaleFactor = ((float) faceBitmap.getWidth()) / ((float) metricsBitmap.getWidth());
|
||||||
|
scaledHeight = Math.round(metricsBitmap.getHeight() * scaleFactor);
|
||||||
|
canvas.drawBitmap(metricsBitmap, null, new Rect(0, 0, faceBitmap.getWidth(), scaledHeight), paint);
|
||||||
|
|
||||||
|
metricsBitmap.recycle();
|
||||||
|
drawingViewBitmap.recycle();
|
||||||
|
|
||||||
|
Date now = new Date();
|
||||||
|
String timestamp = DateFormat.format("yyyy-MM-dd_hh-mm-ss", now).toString();
|
||||||
|
File pictureFolder = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "AffdexMe");
|
||||||
|
if (!pictureFolder.exists()) {
|
||||||
|
if (!pictureFolder.mkdir()) {
|
||||||
|
Log.e(LOG_TAG, "Unable to create directory: " + pictureFolder.getAbsolutePath());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String screenshotFileName = timestamp + ".png";
|
||||||
|
File screenshotFile = new File(pictureFolder, screenshotFileName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ImageHelper.saveBitmapToFileAsPng(finalScreenshot, screenshotFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
String msg = "Unable to save screenshot";
|
||||||
|
Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
|
||||||
|
Log.e(LOG_TAG, msg, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ImageHelper.addPngToGallery(getApplicationContext(), screenshotFile);
|
||||||
|
|
||||||
|
if (alsoSaveRaw) {
|
||||||
|
String rawScreenshotFileName = timestamp + "_raw.png";
|
||||||
|
File rawScreenshotFile = new File(pictureFolder, rawScreenshotFileName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ImageHelper.saveBitmapToFileAsPng(faceBitmap, rawScreenshotFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
String msg = "Unable to save screenshot";
|
||||||
|
Log.e(LOG_TAG, msg, e);
|
||||||
|
}
|
||||||
|
ImageHelper.addPngToGallery(getApplicationContext(), rawScreenshotFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
faceBitmap.recycle();
|
||||||
|
finalScreenshot.recycle();
|
||||||
|
|
||||||
|
String fileSavedMessage = "Screenshot saved to: " + screenshotFile.getPath();
|
||||||
|
Toast.makeText(getApplicationContext(), fileSavedMessage, Toast.LENGTH_SHORT).show();
|
||||||
|
Log.d(LOG_TAG, fileSavedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use the method that we saved in activateMetric() to get the metric score and display it
|
* Use the method that we saved in activateMetric() to get the metric score and display it
|
||||||
*/
|
*/
|
||||||
|
@ -723,6 +898,7 @@ public class MainActivity extends AppCompatActivity
|
||||||
if (b) {
|
if (b) {
|
||||||
settingsButton.setVisibility(View.VISIBLE);
|
settingsButton.setVisibility(View.VISIBLE);
|
||||||
cameraButton.setVisibility(View.VISIBLE);
|
cameraButton.setVisibility(View.VISIBLE);
|
||||||
|
screenshotButton.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
//We display the navigation bar again
|
//We display the navigation bar again
|
||||||
getWindow().getDecorView().setSystemUiVisibility(
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
@ -740,6 +916,7 @@ public class MainActivity extends AppCompatActivity
|
||||||
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
settingsButton.setVisibility(View.INVISIBLE);
|
settingsButton.setVisibility(View.INVISIBLE);
|
||||||
cameraButton.setVisibility(View.INVISIBLE);
|
cameraButton.setVisibility(View.INVISIBLE);
|
||||||
|
screenshotButton.setVisibility(View.INVISIBLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -845,7 +1022,6 @@ public class MainActivity extends AppCompatActivity
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void camera_button_click(View view) {
|
public void camera_button_click(View view) {
|
||||||
//Toggle the camera setting
|
//Toggle the camera setting
|
||||||
setCameraType(cameraType == CameraDetector.CameraType.CAMERA_FRONT ? CameraDetector.CameraType.CAMERA_BACK : CameraDetector.CameraType.CAMERA_FRONT);
|
setCameraType(cameraType == CameraDetector.CameraType.CAMERA_FRONT ? CameraDetector.CameraType.CAMERA_BACK : CameraDetector.CameraType.CAMERA_FRONT);
|
||||||
|
@ -886,6 +1062,14 @@ public class MainActivity extends AppCompatActivity
|
||||||
preferencesEditor.apply();
|
preferencesEditor.apply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBitmapGenerated(@NonNull final Bitmap bitmap) {
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
processScreenshot(bitmap, STORE_RAW_SCREENSHOTS);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
BIN
app/src/main/res/drawable-hdpi/screenshot_button.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/drawable-hdpi/screenshot_button_pressed.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/drawable-mdpi/screenshot_button.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/drawable-mdpi/screenshot_button_pressed.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/drawable-xhdpi/screenshot_button.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
app/src/main/res/drawable-xhdpi/screenshot_button_pressed.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
app/src/main/res/drawable-xxhdpi/screenshot_button.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
app/src/main/res/drawable-xxhdpi/screenshot_button_pressed.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
11
app/src/main/res/drawable/screenshot_button_selector.xml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
- Copyright (c) 2016 Affectiva Inc.
|
||||||
|
- See the file license.txt for copying permission.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/screenshot_button_pressed" android:state_pressed="true" />
|
||||||
|
<item android:drawable="@drawable/screenshot_button" />
|
||||||
|
</selector>
|
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.6 KiB |
|
@ -58,6 +58,19 @@
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:src="@drawable/camera_button_selector" />
|
android:src="@drawable/camera_button_selector" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/screenshot_button"
|
||||||
|
android:layout_width="@dimen/settings_button_size"
|
||||||
|
android:layout_height="@dimen/settings_button_size"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_below="@id/camera_button"
|
||||||
|
android:layout_margin="@dimen/settings_button_margin"
|
||||||
|
android:background="@null"
|
||||||
|
android:contentDescription="Take screenshot"
|
||||||
|
android:onClick="takeScreenshot"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:src="@drawable/screenshot_button_selector" />
|
||||||
|
|
||||||
<include layout="@layout/insufficent_permissions_panel" />
|
<include layout="@layout/insufficent_permissions_panel" />
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="@dimen/bottom_padding"
|
android:layout_marginBottom="@dimen/bottom_padding"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="@string/permissions_needed_explanation" />
|
android:text="@string/permissions_camera_needed_explanation" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/retryPermissionsButton"
|
android:id="@+id/retryPermissionsButton"
|
||||||
|
|
|
@ -14,7 +14,8 @@
|
||||||
<string name="settings_content_description">Settings</string>
|
<string name="settings_content_description">Settings</string>
|
||||||
|
|
||||||
<string name="insufficient_permissions">Insufficient Permissions</string>
|
<string name="insufficient_permissions">Insufficient Permissions</string>
|
||||||
<string name="permissions_needed_explanation">This app requires the permission to access your camera to be able to gather facial images to process</string>
|
<string name="permissions_camera_needed_explanation">This app requires the permission to access your camera to be able to gather facial images for processing. No video is saved or used other than for immediate emotion processing.</string>
|
||||||
|
<string name="permissions_storage_needed_explanation">This app requires the permission to access your external storage to save screenshots.</string>
|
||||||
<string name="error">Error</string>
|
<string name="error">Error</string>
|
||||||
<string name="understood">Understood</string>
|
<string name="understood">Understood</string>
|
||||||
<string name="retry">Retry</string>
|
<string name="retry">Retry</string>
|
||||||
|
|