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
This commit is contained in:
Abraham Hedtke 2016-01-06 19:32:15 -05:00 committed by toby cabot
parent 9a667683bc
commit 3163d6b8fa
18 changed files with 432 additions and 61 deletions

View file

@ -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"

View file

@ -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;

View file

@ -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);
}
} }

View file

@ -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);
}
});
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View 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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -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

View file

@ -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"

View file

@ -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>