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">
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
|
|
|
@ -19,11 +19,13 @@ import android.graphics.Rect;
|
|||
import android.graphics.Typeface;
|
||||
import android.os.Process;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.affectiva.android.affdex.sdk.detector.Face;
|
||||
|
||||
|
@ -52,6 +54,7 @@ public class DrawingView extends SurfaceView implements SurfaceHolder.Callback {
|
|||
private SurfaceHolder surfaceHolder;
|
||||
private DrawingThread drawingThread; //DrawingThread object
|
||||
private DrawingViewConfig drawingViewConfig;
|
||||
private DrawingThreadEventListener listener;
|
||||
|
||||
//three constructors required of any custom view
|
||||
public DrawingView(Context context) {
|
||||
|
@ -73,11 +76,34 @@ public class DrawingView extends SurfaceView implements SurfaceHolder.Callback {
|
|||
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() {
|
||||
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.addCallback(this); //become a Listener to the three events below that SurfaceView generates
|
||||
|
||||
drawingViewConfig = new DrawingViewConfig();
|
||||
|
||||
//Default values
|
||||
|
@ -133,7 +159,7 @@ public class DrawingView extends SurfaceView implements SurfaceHolder.Callback {
|
|||
|
||||
drawingViewConfig.setDominantEmotionLabelPaints(emotionLabelPaint, emotionValuePaint);
|
||||
drawingViewConfig.setDominantEmotionMetricBarConfig(metricBarPaint, metricBarWidth);
|
||||
drawingThread = new DrawingThread(surfaceHolder, drawingViewConfig);
|
||||
drawingThread = new DrawingThread(surfaceHolder, drawingViewConfig, listener);
|
||||
|
||||
//statically load the emoji bitmaps on-demand and cache
|
||||
emojiMarkerBitmapToEmojiTypeMap = new HashMap<>();
|
||||
|
@ -147,7 +173,7 @@ public class DrawingView extends SurfaceView implements SurfaceHolder.Callback {
|
|||
@Override
|
||||
public void surfaceCreated(SurfaceHolder holder) {
|
||||
if (drawingThread.isStopped()) {
|
||||
drawingThread = new DrawingThread(surfaceHolder, drawingViewConfig);
|
||||
drawingThread = new DrawingThread(surfaceHolder, drawingViewConfig, listener);
|
||||
}
|
||||
drawingThread.start();
|
||||
}
|
||||
|
@ -261,6 +287,10 @@ public class DrawingView extends SurfaceView implements SurfaceHolder.Callback {
|
|||
}
|
||||
}
|
||||
|
||||
interface DrawingThreadEventListener {
|
||||
void onBitmapGenerated(Bitmap bitmap);
|
||||
}
|
||||
|
||||
class FacesSharer {
|
||||
boolean isPointsMirrored;
|
||||
List<Face> facesToDraw;
|
||||
|
@ -279,9 +309,11 @@ public class DrawingView extends SurfaceView implements SurfaceHolder.Callback {
|
|||
private Paint boundingBoxPaint;
|
||||
private Paint dominantEmotionScoreBarPaint;
|
||||
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 DrawingThreadEventListener listener;
|
||||
|
||||
public DrawingThread(SurfaceHolder surfaceHolder, DrawingViewConfig con) {
|
||||
public DrawingThread(SurfaceHolder surfaceHolder, DrawingViewConfig con, DrawingThreadEventListener listener) {
|
||||
mSurfaceHolder = surfaceHolder;
|
||||
|
||||
//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;
|
||||
sharer = new FacesSharer();
|
||||
this.listener = listener;
|
||||
|
||||
setThickness(config.drawThickness);
|
||||
}
|
||||
|
||||
public void setEventListener(DrawingThreadEventListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
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.
|
||||
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()
|
||||
* **/
|
||||
Canvas c = null;
|
||||
Canvas screenshotCanvas = null;
|
||||
Bitmap screenshotBitmap = null;
|
||||
try {
|
||||
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) {
|
||||
synchronized (mSurfaceHolder) {
|
||||
c.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); //clear previous dots
|
||||
draw(c);
|
||||
draw(c, screenshotCanvas);
|
||||
}
|
||||
}
|
||||
|
||||
} finally {
|
||||
if (c != null) {
|
||||
mSurfaceHolder.unlockCanvasAndPost(c);
|
||||
}
|
||||
if (screenshotBitmap != null && listener != null) {
|
||||
listener.onBitmapGenerated(Bitmap.createBitmap(screenshotBitmap));
|
||||
screenshotBitmap.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config = null; //nullify object to avoid memory leak
|
||||
}
|
||||
|
||||
void draw(Canvas c) {
|
||||
void draw(@NonNull Canvas c, @Nullable Canvas c2) {
|
||||
Face nextFaceToDraw;
|
||||
boolean mirrorPoints;
|
||||
boolean multiFaceMode;
|
||||
|
@ -400,6 +451,10 @@ public class DrawingView extends SurfaceView implements SurfaceHolder.Callback {
|
|||
|
||||
drawFaceAttributes(c, nextFaceToDraw, mirrorPoints, multiFaceMode);
|
||||
|
||||
if (c2 != null) {
|
||||
drawFaceAttributes(c2, nextFaceToDraw, false, multiFaceMode);
|
||||
}
|
||||
|
||||
synchronized (sharer) {
|
||||
mirrorPoints = sharer.isPointsMirrored;
|
||||
|
||||
|
|
|
@ -5,22 +5,31 @@
|
|||
|
||||
package com.affectiva.affdexme;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.ImageFormat;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.YuvImage;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.provider.MediaStore;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.affectiva.android.affdex.sdk.Frame;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class ImageHelper {
|
||||
|
||||
|
@ -30,7 +39,7 @@ public class 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
|
||||
File directory = context.getDir("images", Context.MODE_PRIVATE);
|
||||
|
@ -41,7 +50,7 @@ public class ImageHelper {
|
|||
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
|
||||
File directory = context.getDir("images", Context.MODE_PRIVATE);
|
||||
|
||||
|
@ -51,7 +60,7 @@ public class ImageHelper {
|
|||
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());
|
||||
|
||||
if (resourceId == 0) {
|
||||
|
@ -61,7 +70,7 @@ public class ImageHelper {
|
|||
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();
|
||||
Bitmap sourceBitmap = BitmapFactory.decodeResource(resources, resourceId);
|
||||
Bitmap resizedBitmap = resizeBitmapForDeviceDensity(context, sourceBitmap);
|
||||
|
@ -70,7 +79,7 @@ public class ImageHelper {
|
|||
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();
|
||||
|
||||
int targetWidth = Math.round(sourceBitmap.getWidth() * metrics.density);
|
||||
|
@ -79,7 +88,7 @@ public class ImageHelper {
|
|||
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
|
||||
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
|
||||
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
|
||||
final boolean DEBUG = false;
|
||||
|
||||
|
@ -157,10 +166,10 @@ public class ImageHelper {
|
|||
* @param imageView source ImageView
|
||||
* @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];
|
||||
|
||||
if (imageView == null || imageView.getDrawable() == null)
|
||||
if (imageView.getDrawable() == null)
|
||||
return ret;
|
||||
|
||||
// Get image dimensions
|
||||
|
@ -197,4 +206,101 @@ public class ImageHelper {
|
|||
|
||||
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.SharedPreferences;
|
||||
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.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.SystemClock;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.app.AlertDialog;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.text.format.DateFormat;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
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.Face;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
|
@ -80,17 +88,20 @@ import java.util.Locale;
|
|||
|
||||
public class MainActivity extends AppCompatActivity
|
||||
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;
|
||||
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 cameraPreviewHeight = 0;
|
||||
CameraDetector.CameraType cameraType;
|
||||
boolean mirrorPoints = false;
|
||||
private boolean cameraPermissionsAvailable = false;
|
||||
private boolean storagePermissionsAvailable = false;
|
||||
private CameraDetector detector = null;
|
||||
private RelativeLayout metricViewLayout;
|
||||
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 ImageButton settingsButton;
|
||||
private ImageButton cameraButton;
|
||||
private ImageButton screenshotButton;
|
||||
private Frame mostRecentFrame;
|
||||
private boolean isMenuVisible = false;
|
||||
private boolean isFPSVisible = false;
|
||||
private boolean isMenuShowingForFirstTime = true;
|
||||
|
@ -125,7 +138,7 @@ public class MainActivity extends AppCompatActivity
|
|||
preproccessMetricImages();
|
||||
setContentView(R.layout.activity_main);
|
||||
initializeUI();
|
||||
checkForDangerousPermissions();
|
||||
checkForCameraPermissions();
|
||||
determineCameraAvailability();
|
||||
initializeCameraDetector();
|
||||
}
|
||||
|
@ -153,8 +166,7 @@ public class MainActivity extends AppCompatActivity
|
|||
ImageHelper.preproccessImageIfNecessary(context, "unknown_noglasses.png", "unknown_noglasses");
|
||||
}
|
||||
|
||||
|
||||
private void checkForDangerousPermissions() {
|
||||
private void checkForCameraPermissions() {
|
||||
cameraPermissionsAvailable =
|
||||
ContextCompat.checkSelfPermission(
|
||||
getApplicationContext(),
|
||||
|
@ -168,35 +180,67 @@ public class MainActivity extends AppCompatActivity
|
|||
// 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();
|
||||
showPermissionExplanationDialog(CAMERA_PERMISSIONS_REQUEST);
|
||||
} else {
|
||||
// No explanation needed, we can request the permission.
|
||||
requestNeededPermissions();
|
||||
requestCameraPermissions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void requestNeededPermissions() {
|
||||
List<String> neededPermissions = new ArrayList<>();
|
||||
private void checkForStoragePermissions() {
|
||||
storagePermissionsAvailable =
|
||||
ContextCompat.checkSelfPermission(
|
||||
getApplicationContext(),
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
|
||||
|
||||
if (!cameraPermissionsAvailable) {
|
||||
neededPermissions.add(Manifest.permission.CAMERA);
|
||||
if (!storagePermissionsAvailable) {
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
private void requestStoragePermissions() {
|
||||
if (!storagePermissionsAvailable) {
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
neededPermissions.toArray(new String[neededPermissions.size()]),
|
||||
AFFDEXME_PERMISSIONS_REQUEST);
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
||||
if (requestCode == AFFDEXME_PERMISSIONS_REQUEST) {
|
||||
if (requestCode == CAMERA_PERMISSIONS_REQUEST) {
|
||||
for (int i = 0; i < permissions.length; i++) {
|
||||
String permission = permissions[i];
|
||||
int grantResult = grantResults[i];
|
||||
|
@ -205,7 +249,6 @@ public class MainActivity extends AppCompatActivity
|
|||
cameraPermissionsAvailable = (grantResult == PackageManager.PERMISSION_GRANTED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!cameraPermissionsAvailable) {
|
||||
permissionsUnavailableLayout.setVisibility(View.VISIBLE);
|
||||
|
@ -214,23 +257,53 @@ public class MainActivity extends AppCompatActivity
|
|||
}
|
||||
}
|
||||
|
||||
private void showPermissionExplanationDialog() {
|
||||
if (requestCode == EXTERNAL_STORAGE_PERMISSIONS_REQUEST) {
|
||||
for (int i = 0; i < permissions.length; i++) {
|
||||
String permission = permissions[i];
|
||||
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(int requestCode) {
|
||||
final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(
|
||||
getApplicationContext());
|
||||
MainActivity.this);
|
||||
|
||||
// set title
|
||||
alertDialogBuilder.setTitle(getResources().getString(R.string.insufficient_permissions));
|
||||
|
||||
// set dialog message
|
||||
if (requestCode == CAMERA_PERMISSIONS_REQUEST) {
|
||||
alertDialogBuilder
|
||||
.setMessage(getResources().getString(R.string.permissions_needed_explanation))
|
||||
.setMessage(getResources().getString(R.string.permissions_camera_needed_explanation))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(getResources().getString(R.string.understood), new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dialog.cancel();
|
||||
requestNeededPermissions();
|
||||
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
|
||||
AlertDialog alertDialog = alertDialogBuilder.create();
|
||||
|
@ -239,7 +312,6 @@ public class MainActivity extends AppCompatActivity
|
|||
alertDialog.show();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
@ -287,6 +359,7 @@ public class MainActivity extends AppCompatActivity
|
|||
drawingView = (DrawingView) findViewById(R.id.drawing_view);
|
||||
settingsButton = (ImageButton) findViewById(R.id.settings_button);
|
||||
cameraButton = (ImageButton) findViewById(R.id.camera_button);
|
||||
screenshotButton = (ImageButton) findViewById(R.id.screenshot_button);
|
||||
progressBar = (ProgressBar) findViewById(R.id.progress_bar);
|
||||
pleaseWaitTextView = (TextView) findViewById(R.id.please_wait_textview);
|
||||
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
|
||||
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
|
||||
* 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() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
requestNeededPermissions();
|
||||
requestCameraPermissions();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -381,7 +457,7 @@ public class MainActivity extends AppCompatActivity
|
|||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
checkForDangerousPermissions();
|
||||
checkForCameraPermissions();
|
||||
restoreApplicationSettings();
|
||||
setMenuVisible(true);
|
||||
isMenuShowingForFirstTime = true;
|
||||
|
@ -576,7 +652,6 @@ public class MainActivity extends AppCompatActivity
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onFaceDetectionStarted() {
|
||||
leftMetricsLayout.animate().alpha(1); //make left and right metrics appear
|
||||
|
@ -601,6 +676,8 @@ public class MainActivity extends AppCompatActivity
|
|||
*/
|
||||
@Override
|
||||
public void onImageResults(List<Face> faces, Frame image, float timeStamp) {
|
||||
mostRecentFrame = image;
|
||||
|
||||
//If the faces object is null, we received an unprocessed frame
|
||||
if (faces == null) {
|
||||
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
|
||||
*/
|
||||
|
@ -723,6 +898,7 @@ public class MainActivity extends AppCompatActivity
|
|||
if (b) {
|
||||
settingsButton.setVisibility(View.VISIBLE);
|
||||
cameraButton.setVisibility(View.VISIBLE);
|
||||
screenshotButton.setVisibility(View.VISIBLE);
|
||||
|
||||
//We display the navigation bar again
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
|
@ -740,6 +916,7 @@ public class MainActivity extends AppCompatActivity
|
|||
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||
settingsButton.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) {
|
||||
//Toggle the camera setting
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@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: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" />
|
||||
|
||||
<RelativeLayout
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/bottom_padding"
|
||||
android:gravity="center"
|
||||
android:text="@string/permissions_needed_explanation" />
|
||||
android:text="@string/permissions_camera_needed_explanation" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/retryPermissionsButton"
|
||||
|
|
|
@ -14,7 +14,8 @@
|
|||
<string name="settings_content_description">Settings</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="understood">Understood</string>
|
||||
<string name="retry">Retry</string>
|
||||
|
|