add example app
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e80e2d2..d780c39 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -21,9 +21,9 @@
java-version: 11
distribution: adopt
- name: Gradle Build Action
- run: ./gradlew :app:assembleDebug
+ run: ./gradlew :exampleApp:assembleDebug
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: app-debug.apk
- path: app/build/outputs/apk/debug/app-debug.apk
+ path: exampleApp/build/outputs/apk/debug/exampleApp-debug.apk
diff --git a/FaceShared/build.gradle b/FaceShared/build.gradle
index 7fa02cf..8c48df2 100644
--- a/FaceShared/build.gradle
+++ b/FaceShared/build.gradle
@@ -58,4 +58,12 @@
dependencies {
implementation('androidx.annotation:annotation:1.6.0')
implementation('org.tensorflow:tensorflow-lite:2.11.0')
+ constraints {
+ implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0") {
+ because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib")
+ }
+ implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0") {
+ because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib")
+ }
+ }
}
\ No newline at end of file
diff --git a/exampleApp/.gitignore b/exampleApp/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/exampleApp/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/exampleApp/build.gradle b/exampleApp/build.gradle
new file mode 100644
index 0000000..a7d9c9c
--- /dev/null
+++ b/exampleApp/build.gradle
@@ -0,0 +1,43 @@
+plugins {
+ id 'com.android.application'
+}
+
+android {
+ namespace 'com.libremobileos.facedetect'
+ compileSdk 33
+
+ defaultConfig {
+ applicationId "com.libremobileos.yifan.face.example"
+ minSdk 28
+ targetSdk 33
+ versionCode 1
+ versionName "1.0"
+ missingDimensionStrategy 'gpu', 'withGpu' // include gpu delegate support. withoutGpu = exclude it
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_11
+ targetCompatibility JavaVersion.VERSION_11
+ }
+}
+
+dependencies {
+ implementation('androidx.annotation:annotation:1.6.0')
+ implementation('androidx.appcompat:appcompat:1.6.1')
+ implementation(project(':FaceShared'))
+
+ def camerax_version = "1.3.0-alpha04"
+ implementation "androidx.camera:camera-core:${camerax_version}"
+ implementation "androidx.camera:camera-camera2:${camerax_version}"
+ implementation "androidx.camera:camera-lifecycle:${camerax_version}"
+ implementation "androidx.camera:camera-video:${camerax_version}"
+ implementation "androidx.camera:camera-view:${camerax_version}"
+ implementation "androidx.camera:camera-extensions:${camerax_version}"
+ implementation "androidx.exifinterface:exifinterface:1.3.6"
+}
\ No newline at end of file
diff --git a/exampleApp/proguard-rules.pro b/exampleApp/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/exampleApp/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/exampleApp/src/main/AndroidManifest.xml b/exampleApp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..108779a
--- /dev/null
+++ b/exampleApp/src/main/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <uses-feature
+ android:name="android.hardware.camera"
+ android:required="true" />
+
+ <uses-permission android:name="android.permission.CAMERA" />
+
+ <application
+ android:allowBackup="true"
+ android:dataExtractionRules="@xml/data_extraction_rules"
+ android:fullBackupContent="@xml/backup_rules"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.FaceDetect"
+ tools:targetApi="31">
+ <activity
+ android:name=".MainActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
\ No newline at end of file
diff --git a/exampleApp/src/main/java/com/libremobileos/facedetect/BitmapUtils.java b/exampleApp/src/main/java/com/libremobileos/facedetect/BitmapUtils.java
new file mode 100644
index 0000000..dbc303b
--- /dev/null
+++ b/exampleApp/src/main/java/com/libremobileos/facedetect/BitmapUtils.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright 2020 Google LLC. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * File imported without required modifications, only fixing IDE warnings.
+ * Source: https://github.com/googlesamples/mlkit/blob/d10c447f8259b59262582c30c1608cdf38f4e4a0/android/vision-quickstart/app/src/main/java/com/google/mlkit/vision/demo/BitmapUtils.java
+ */
+
+package com.libremobileos.facedetect;
+
+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.media.Image;
+import android.media.Image.Plane;
+import android.util.Log;
+import androidx.annotation.Nullable;
+import androidx.camera.core.ExperimentalGetImage;
+import androidx.camera.core.ImageProxy;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+/** Utils functions for bitmap conversions. */
+public class BitmapUtils {
+ /** Describing a frame info. */
+ public static class FrameMetadata {
+
+ private final int width;
+ private final int height;
+ private final int rotation;
+
+ public int getWidth() {
+ return width;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public int getRotation() {
+ return rotation;
+ }
+
+ private FrameMetadata(int width, int height, int rotation) {
+ this.width = width;
+ this.height = height;
+ this.rotation = rotation;
+ }
+
+ /** Builder of {@link FrameMetadata}. */
+ public static class Builder {
+
+ private int width;
+ private int height;
+ private int rotation;
+
+ public Builder setWidth(int width) {
+ this.width = width;
+ return this;
+ }
+
+ public Builder setHeight(int height) {
+ this.height = height;
+ return this;
+ }
+
+ public Builder setRotation(int rotation) {
+ this.rotation = rotation;
+ return this;
+ }
+
+ public FrameMetadata build() {
+ return new FrameMetadata(width, height, rotation);
+ }
+ }
+ }
+
+ /** Converts NV21 format byte buffer to bitmap. */
+ @Nullable
+ public static Bitmap getBitmap(ByteBuffer data, FrameMetadata metadata) {
+ data.rewind();
+ byte[] imageInBuffer = new byte[data.limit()];
+ data.get(imageInBuffer, 0, imageInBuffer.length);
+ try {
+ YuvImage image =
+ new YuvImage(
+ imageInBuffer, ImageFormat.NV21, metadata.getWidth(), metadata.getHeight(), null);
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ image.compressToJpeg(new Rect(0, 0, metadata.getWidth(), metadata.getHeight()), 80, stream);
+
+ Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
+
+ stream.close();
+ return rotateBitmap(bmp, metadata.getRotation());
+ } catch (Exception e) {
+ Log.e("VisionProcessorBase", "Error: " + e.getMessage());
+ }
+ return null;
+ }
+
+ /** Converts a YUV_420_888 image from CameraX API to a bitmap. */
+ @Nullable
+ @ExperimentalGetImage
+ public static Bitmap getBitmap(ImageProxy image) {
+ FrameMetadata frameMetadata =
+ new FrameMetadata.Builder()
+ .setWidth(image.getWidth())
+ .setHeight(image.getHeight())
+ .setRotation(image.getImageInfo().getRotationDegrees())
+ .build();
+
+ ByteBuffer nv21Buffer =
+ yuv420ThreePlanesToNV21(Objects.requireNonNull(image.getImage()).getPlanes(), image.getWidth(), image.getHeight());
+ return getBitmap(nv21Buffer, frameMetadata);
+ }
+
+ /** Rotates a bitmap if it is converted from a bytebuffer. */
+ private static Bitmap rotateBitmap(
+ Bitmap bitmap, int rotationDegrees) {
+ Matrix matrix = new Matrix();
+
+ // Rotate the image back to straight.
+ matrix.postRotate(rotationDegrees);
+
+ Bitmap rotatedBitmap =
+ Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
+
+ // Recycle the old bitmap if it has changed.
+ if (rotatedBitmap != bitmap) {
+ bitmap.recycle();
+ }
+ return rotatedBitmap;
+ }
+
+ /**
+ * Converts YUV_420_888 to NV21 bytebuffer.
+ *
+ * <p>The NV21 format consists of a single byte array containing the Y, U and V values. For an
+ * image of size S, the first S positions of the array contain all the Y values. The remaining
+ * positions contain interleaved V and U values. U and V are subsampled by a factor of 2 in both
+ * dimensions, so there are S/4 U values and S/4 V values. In summary, the NV21 array will contain
+ * S Y values followed by S/4 VU values: YYYYYYYYYYYYYY(...)YVUVUVUVU(...)VU
+ *
+ * <p>YUV_420_888 is a generic format that can describe any YUV image where U and V are subsampled
+ * by a factor of 2 in both dimensions. {@link Image#getPlanes} returns an array with the Y, U and
+ * V planes. The Y plane is guaranteed not to be interleaved, so we can just copy its values into
+ * the first part of the NV21 array. The U and V planes may already have the representation in the
+ * NV21 format. This happens if the planes share the same buffer, the V buffer is one position
+ * before the U buffer and the planes have a pixelStride of 2. If this is case, we can just copy
+ * them to the NV21 array.
+ */
+ private static ByteBuffer yuv420ThreePlanesToNV21(
+ Plane[] yuv420888planes, int width, int height) {
+ int imageSize = width * height;
+ byte[] out = new byte[imageSize + 2 * (imageSize / 4)];
+
+ if (areUVPlanesNV21(yuv420888planes, width, height)) {
+ // Copy the Y values.
+ yuv420888planes[0].getBuffer().get(out, 0, imageSize);
+
+ ByteBuffer uBuffer = yuv420888planes[1].getBuffer();
+ ByteBuffer vBuffer = yuv420888planes[2].getBuffer();
+ // Get the first V value from the V buffer, since the U buffer does not contain it.
+ vBuffer.get(out, imageSize, 1);
+ // Copy the first U value and the remaining VU values from the U buffer.
+ uBuffer.get(out, imageSize + 1, 2 * imageSize / 4 - 1);
+ } else {
+ // Fallback to copying the UV values one by one, which is slower but also works.
+ // Unpack Y.
+ unpackPlane(yuv420888planes[0], width, height, out, 0, 1);
+ // Unpack U.
+ unpackPlane(yuv420888planes[1], width, height, out, imageSize + 1, 2);
+ // Unpack V.
+ unpackPlane(yuv420888planes[2], width, height, out, imageSize, 2);
+ }
+
+ return ByteBuffer.wrap(out);
+ }
+
+ /** Checks if the UV plane buffers of a YUV_420_888 image are in the NV21 format. */
+ private static boolean areUVPlanesNV21(Plane[] planes, int width, int height) {
+ int imageSize = width * height;
+
+ ByteBuffer uBuffer = planes[1].getBuffer();
+ ByteBuffer vBuffer = planes[2].getBuffer();
+
+ // Backup buffer properties.
+ int vBufferPosition = vBuffer.position();
+ int uBufferLimit = uBuffer.limit();
+
+ // Advance the V buffer by 1 byte, since the U buffer will not contain the first V value.
+ vBuffer.position(vBufferPosition + 1);
+ // Chop off the last byte of the U buffer, since the V buffer will not contain the last U value.
+ uBuffer.limit(uBufferLimit - 1);
+
+ // Check that the buffers are equal and have the expected number of elements.
+ boolean areNV21 =
+ (vBuffer.remaining() == (2 * imageSize / 4 - 2)) && (vBuffer.compareTo(uBuffer) == 0);
+
+ // Restore buffers to their initial state.
+ vBuffer.position(vBufferPosition);
+ uBuffer.limit(uBufferLimit);
+
+ return areNV21;
+ }
+
+ /**
+ * Unpack an image plane into a byte array.
+ *
+ * <p>The input plane data will be copied in 'out', starting at 'offset' and every pixel will be
+ * spaced by 'pixelStride'. Note that there is no row padding on the output.
+ */
+ private static void unpackPlane(
+ Plane plane, int width, int height, byte[] out, int offset, int pixelStride) {
+ ByteBuffer buffer = plane.getBuffer();
+ buffer.rewind();
+
+ // Compute the size of the current plane.
+ // We assume that it has the aspect ratio as the original image.
+ int numRow = (buffer.limit() + plane.getRowStride() - 1) / plane.getRowStride();
+ if (numRow == 0) {
+ return;
+ }
+ int scaleFactor = height / numRow;
+ int numCol = width / scaleFactor;
+
+ // Extract the data in the output buffer.
+ int outputPos = offset;
+ int rowStart = 0;
+ for (int row = 0; row < numRow; row++) {
+ int inputPos = rowStart;
+ for (int col = 0; col < numCol; col++) {
+ out[outputPos] = buffer.get(inputPos);
+ outputPos += pixelStride;
+ inputPos += plane.getPixelStride();
+ }
+ rowStart += plane.getRowStride();
+ }
+ }
+}
\ No newline at end of file
diff --git a/exampleApp/src/main/java/com/libremobileos/facedetect/FaceBoundsOverlayView.java b/exampleApp/src/main/java/com/libremobileos/facedetect/FaceBoundsOverlayView.java
new file mode 100644
index 0000000..8da8b9e
--- /dev/null
+++ b/exampleApp/src/main/java/com/libremobileos/facedetect/FaceBoundsOverlayView.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2023 LibreMobileOS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.libremobileos.facedetect;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.util.Pair;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.libremobileos.yifan.face.ImageUtils;
+
+import java.util.List;
+
+public class FaceBoundsOverlayView extends View {
+
+ private List<Pair<RectF, String>> bounds = null;
+ private Paint paint, textPaint;
+ private Matrix transform = null;
+ private int extraWidth, extraHeight, viewWidth, viewHeight, sensorWidth, sensorHeight;
+
+ public FaceBoundsOverlayView(Context context) {
+ this(context, null);
+ }
+
+ public FaceBoundsOverlayView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public FaceBoundsOverlayView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public FaceBoundsOverlayView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (bounds == null || transform == null || paint == null)
+ return; // am I ready yet?
+ for (Pair<RectF, String> bound : bounds) {
+ canvas.drawRect(bound.first, paint);
+ if (bound.second != null)
+ canvas.drawText(bound.second, bound.first.left, bound.first.bottom, textPaint);
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldWidth, int oldHeight) {
+ super.onSizeChanged(w, h, oldWidth, oldHeight);
+ viewWidth = w;
+ viewHeight = h;
+ transform = null;
+ }
+
+ // please give me RectF's that wont be used otherwise as I modify them
+ public void updateBounds(List<Pair<RectF, String>> inputBounds, int sensorWidth, int sensorHeight) {
+ this.bounds = inputBounds;
+ // if we have no paint yet, make one
+ if (paint == null) {
+ paint = new Paint();
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeWidth(10f);
+ paint.setColor(Color.RED);
+ }
+ if (textPaint == null) {
+ textPaint = new Paint();
+ textPaint.setColor(Color.RED);
+ textPaint.setTextSize(100);
+ }
+ // if camera size or view size changed, recalculate it
+ if (this.sensorWidth != sensorWidth || this.sensorHeight != sensorHeight || (viewWidth + viewHeight) > 0) {
+ this.sensorWidth = sensorWidth;
+ this.sensorHeight = sensorHeight;
+ int oldWidth = viewWidth;
+ int oldHeight = viewHeight;
+ extraWidth = 0;
+ extraHeight = 0;
+ // calculate scaling keeping aspect ratio
+ int newHeight = (int)((oldWidth / (float)sensorWidth) * sensorHeight);
+ int newWidth = (int)((oldHeight / (float)sensorHeight) * sensorWidth);
+ // calculate out black bars
+ if (newWidth > oldWidth) {
+ extraHeight = (oldHeight - newHeight) / 2;
+ viewHeight = newHeight;
+ } else {
+ extraWidth = (oldWidth - newWidth) / 2;
+ viewWidth = newWidth;
+ }
+ // scale from image size to view size
+ transform = ImageUtils.getTransformationMatrix(sensorWidth, sensorHeight, viewWidth, viewHeight, 0, false);
+ viewWidth = 0; viewHeight = 0;
+ }
+ // map bounds to view size
+ for (Pair<RectF, String> bound : bounds) {
+ transform.mapRect(bound.first);
+ bound.first.offset(extraWidth, extraHeight);
+ }
+ invalidate();
+ }
+}
diff --git a/exampleApp/src/main/java/com/libremobileos/facedetect/MainActivity.java b/exampleApp/src/main/java/com/libremobileos/facedetect/MainActivity.java
new file mode 100644
index 0000000..a3243d1
--- /dev/null
+++ b/exampleApp/src/main/java/com/libremobileos/facedetect/MainActivity.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2023 LibreMobileOS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.libremobileos.facedetect;
+
+import android.content.res.Configuration;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.os.Bundle;
+import android.util.Pair;
+import android.util.Size;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.ExperimentalGetImage;
+import androidx.camera.core.ImageAnalysis;
+import androidx.camera.core.Preview;
+import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.camera.view.PreviewView;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import com.libremobileos.yifan.face.FaceRecognizer;
+import com.libremobileos.yifan.face.FaceStorageBackend;
+import com.libremobileos.yifan.face.SharedPreferencesFaceStorageBackend;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+public class MainActivity extends AppCompatActivity {
+
+ // CameraX boilerplate
+ private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
+ // View showing camera frames
+ private PreviewView previewView;
+ // AI-based detector
+ private FaceRecognizer faceRecognizer;
+ // Simple view allowing us to draw Rectangles over the Preview
+ private FaceBoundsOverlayView overlayView;
+ // The desired camera input size
+ private final Size desiredInputSize = new Size(640, 480);
+ // The calculated actual processing width & height
+ private int width, height;
+ // Store registered Faces in Memory
+ private FaceStorageBackend faceStorage;
+ // If we are waiting for a face to be added to knownFaces
+ private boolean addPending = false;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ // Initialize basic views
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ previewView = findViewById(R.id.viewFinder);
+ previewView.setScaleType(PreviewView.ScaleType.FIT_CENTER);
+ overlayView = findViewById(R.id.overlay);
+ overlayView.setOnClickListener(v -> addPending = true);
+ setTitle(getString(R.string.tap_to_add_face));
+
+ // CameraX boilerplate (create camera connection)
+ cameraProviderFuture = ProcessCameraProvider.getInstance(this);
+ cameraProviderFuture.addListener(() -> {
+ try {
+ ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
+ bindPreview(cameraProvider);
+ } catch (ExecutionException | InterruptedException e) {
+ // No errors need to be handled for this Future.
+ // This should never be reached.
+ }
+ }, getMainExecutor());
+
+ }
+
+ @OptIn(markerClass = ExperimentalGetImage.class)
+ private void bindPreview(@NonNull ProcessCameraProvider cameraProvider) {
+ // We're connected to the camera, set up everything
+ Preview preview = new Preview.Builder()
+ .build();
+
+ // Which camera to use
+ int selectedCamera = CameraSelector.LENS_FACING_FRONT;
+ CameraSelector cameraSelector = new CameraSelector.Builder()
+ .requireLensFacing(selectedCamera)
+ .build();
+
+ preview.setSurfaceProvider(previewView.getSurfaceProvider());
+
+ // Cameras give us landscape images. If we are in portrait mode
+ // (and want to process a portrait image), swap width/height to
+ // make the image portrait.
+ if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
+ width = desiredInputSize.getHeight();
+ height = desiredInputSize.getWidth();
+ } else {
+ width = desiredInputSize.getWidth();
+ height = desiredInputSize.getHeight();
+ }
+
+ // Set up CameraX boilerplate and configure it to drop frames if we can't keep up
+ ImageAnalysis imageAnalysis =
+ new ImageAnalysis.Builder()
+ .setTargetResolution(new Size(width, height))
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
+ .build();
+
+ imageAnalysis.setAnalyzer(getMainExecutor(), imageProxy -> {
+ // Convert CameraX Image to Bitmap and process it
+ // Return list of detected faces
+ List<FaceRecognizer.Face> data = faceRecognizer.recognize(BitmapUtils.getBitmap(imageProxy));
+ ArrayList<Pair<RectF, String>> bounds = new ArrayList<>();
+
+ for (FaceRecognizer.Face face : data) {
+ RectF boundingBox = new RectF(face.getLocation());
+
+ // Camera is frontal so the image is flipped horizontally,
+ // so flip it again.
+ Matrix flip = new Matrix();
+ flip.postScale(-1, 1, width / 2.0f, height / 2.0f);
+ flip.mapRect(boundingBox);
+
+ // Generate UI text for face
+ String uiText;
+ // Do we want to add a new face?
+ if (addPending) {
+ // If we want to add a new face, show the dialog.
+ runOnUiThread(() -> showAddFaceDialog(face));
+ addPending = false;
+ }
+ // Do we have any match?
+ if (face.isRecognized()) {
+ // If yes, show the user-visible ID and the detection confidence
+ uiText = face.getModelCount() + " " + face.getTitle() + " " + face.getDistance();
+ } else {
+ // Show detected object type (always "Face") and how confident the AI is that this is a Face
+ uiText = face.getTitle() + " " + face.getDetectionConfidence();
+ }
+ bounds.add(new Pair<>(boundingBox, uiText));
+ }
+
+ // Pass bounds to View drawing rectangles
+ overlayView.updateBounds(bounds, width, height);
+ // Clean up
+ imageProxy.close();
+ });
+
+ // Bind all objects together
+ /* Camera camera = */ cameraProvider.bindToLifecycle(this, cameraSelector, imageAnalysis, preview);
+
+ // Create AI-based face detection
+ //faceStorage = new VolatileFaceStorageBackend();
+ faceStorage = new SharedPreferencesFaceStorageBackend(getSharedPreferences("faces", 0));
+ faceRecognizer = FaceRecognizer.create(this,
+ faceStorage, /* face data storage */
+ 0.6f, /* minimum confidence to consider object as face */
+ width, /* bitmap width */
+ height, /* bitmap height */
+ 0, /* CameraX rotates the image for us, so we chose to IGNORE sensorRotation altogether */
+ 0.7f, /* maximum distance to track face */
+ 1 /* minimum model count to track face */
+ );
+ }
+
+ private void showAddFaceDialog(FaceRecognizer.Face rec) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ LayoutInflater inflater = getLayoutInflater();
+ View dialogLayout = inflater.inflate(R.layout.image_edit_dialog, null);
+ ImageView ivFace = dialogLayout.findViewById(R.id.dlg_image);
+ TextView tvTitle = dialogLayout.findViewById(R.id.dlg_title);
+ EditText etName = dialogLayout.findViewById(R.id.dlg_input);
+
+ tvTitle.setText(R.string.add_face);
+ // Add preview of cropped face to verify we're adding the correct one
+ ivFace.setImageBitmap(rec.getCrop());
+ etName.setHint(R.string.input_name);
+
+ builder.setPositiveButton(R.string.ok, (dlg, i) -> {
+ String name = etName.getText().toString();
+ if (name.isEmpty()) {
+ return;
+ }
+ // Save facial features in knownFaces
+ if (!faceStorage.extendRegistered(name, rec.getExtra(), true)) {
+ Toast.makeText(this, R.string.register_failed, Toast.LENGTH_LONG).show();
+ }
+ dlg.dismiss();
+ });
+ builder.setView(dialogLayout);
+ builder.show();
+ }
+
+}
diff --git a/exampleApp/src/main/res/drawable-v24/ic_launcher_foreground.xml b/exampleApp/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..c6aee64
--- /dev/null
+++ b/exampleApp/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:endX="85.84757"
+ android:endY="92.4963"
+ android:startX="42.9492"
+ android:startY="49.59793"
+ android:type="linear">
+ <item
+ android:color="#44000000"
+ android:offset="0.0" />
+ <item
+ android:color="#00000000"
+ android:offset="1.0" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:fillColor="#FFFFFF"
+ android:fillType="nonZero"
+ android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+ android:strokeWidth="1"
+ android:strokeColor="#00000000" />
+</vector>
\ No newline at end of file
diff --git a/exampleApp/src/main/res/drawable/ic_launcher_background.xml b/exampleApp/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..4e12b43
--- /dev/null
+++ b/exampleApp/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path
+ android:fillColor="#3DDC84"
+ android:pathData="M0,0h108v108h-108z" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M9,0L9,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,0L19,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,0L29,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,0L39,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,0L49,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,0L59,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,0L69,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,0L79,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M89,0L89,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M99,0L99,108"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,9L108,9"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,19L108,19"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,29L108,29"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,39L108,39"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,49L108,49"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,59L108,59"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,69L108,69"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,79L108,79"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,89L108,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M0,99L108,99"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,29L89,29"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,39L89,39"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,49L89,49"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,59L89,59"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,69L89,69"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M19,79L89,79"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M29,19L29,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M39,19L39,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M49,19L49,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M59,19L59,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M69,19L69,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+ <path
+ android:fillColor="#00000000"
+ android:pathData="M79,19L79,89"
+ android:strokeWidth="0.8"
+ android:strokeColor="#33FFFFFF" />
+</vector>
diff --git a/exampleApp/src/main/res/layout/activity_main.xml b/exampleApp/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..1f0ca0c
--- /dev/null
+++ b/exampleApp/src/main/res/layout/activity_main.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <androidx.camera.view.PreviewView
+ android:id="@+id/viewFinder"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <com.libremobileos.facedetect.FaceBoundsOverlayView
+ android:id="@+id/overlay"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/exampleApp/src/main/res/layout/image_edit_dialog.xml b/exampleApp/src/main/res/layout/image_edit_dialog.xml
new file mode 100644
index 0000000..60e46ca
--- /dev/null
+++ b/exampleApp/src/main/res/layout/image_edit_dialog.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:orientation="vertical"
+ android:padding="16dp">
+
+ <TextView
+ android:id="@+id/dlg_title"
+ android:layout_gravity="center"
+ android:textSize="20sp"
+ tools:text="The dialog title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <ImageView
+ android:layout_gravity="center"
+ android:id="@+id/dlg_image"
+ android:layout_width="200dp"
+ android:layout_height="200dp"
+ android:scaleType="centerCrop"
+ android:adjustViewBounds="true"
+ />
+
+ <EditText
+ android:layout_gravity="center"
+ android:id="@+id/dlg_input"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ tools:hint="The dialog hint"
+ />
+
+
+</LinearLayout>
\ No newline at end of file
diff --git a/exampleApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/exampleApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..65bd9d3
--- /dev/null
+++ b/exampleApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/exampleApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/exampleApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..65bd9d3
--- /dev/null
+++ b/exampleApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/exampleApp/src/main/res/mipmap-anydpi-v33/ic_launcher.xml b/exampleApp/src/main/res/mipmap-anydpi-v33/ic_launcher.xml
new file mode 100644
index 0000000..52ac069
--- /dev/null
+++ b/exampleApp/src/main/res/mipmap-anydpi-v33/ic_launcher.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+ <monochrome android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/exampleApp/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/exampleApp/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..4eaccdd
--- /dev/null
+++ b/exampleApp/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/exampleApp/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/exampleApp/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..7baaea0
--- /dev/null
+++ b/exampleApp/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/exampleApp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/exampleApp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..9b01b6d
--- /dev/null
+++ b/exampleApp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/exampleApp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/exampleApp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..bfc2f07
--- /dev/null
+++ b/exampleApp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/exampleApp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/exampleApp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..0ee89d8
--- /dev/null
+++ b/exampleApp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/exampleApp/src/main/res/values/colors.xml b/exampleApp/src/main/res/values/colors.xml
new file mode 100644
index 0000000..d95bf93
--- /dev/null
+++ b/exampleApp/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="purple_200">#FFBB86FC</color>
+ <color name="purple_500">#FF6200EE</color>
+ <color name="purple_700">#FF3700B3</color>
+ <color name="teal_200">#FF03DAC5</color>
+ <color name="teal_700">#FF018786</color>
+ <color name="black">#FF000000</color>
+ <color name="white">#FFFFFFFF</color>
+</resources>
\ No newline at end of file
diff --git a/exampleApp/src/main/res/values/strings.xml b/exampleApp/src/main/res/values/strings.xml
new file mode 100644
index 0000000..573d092
--- /dev/null
+++ b/exampleApp/src/main/res/values/strings.xml
@@ -0,0 +1,8 @@
+<resources>
+ <string name="app_name">FaceDetect</string>
+ <string name="add_face">Add Face</string>
+ <string name="input_name">Input name</string>
+ <string name="ok">OK</string>
+ <string name="tap_to_add_face">Tap anywhere to add face</string>
+ <string name="register_failed">Registering the face failed.</string>
+</resources>
\ No newline at end of file
diff --git a/exampleApp/src/main/res/values/themes.xml b/exampleApp/src/main/res/values/themes.xml
new file mode 100644
index 0000000..aaa8e1b
--- /dev/null
+++ b/exampleApp/src/main/res/values/themes.xml
@@ -0,0 +1,6 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <!-- Base application theme. -->
+ <style name="Theme.FaceDetect" parent="Theme.AppCompat.DayNight">
+ <!-- Customize your theme here. -->
+ </style>
+</resources>
\ No newline at end of file
diff --git a/exampleApp/src/main/res/xml/backup_rules.xml b/exampleApp/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..26457c5
--- /dev/null
+++ b/exampleApp/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Sample backup rules file; uncomment and customize as necessary.
+ See https://developer.android.com/guide/topics/data/autobackup
+ for details.
+ Note: This file is ignored for devices older that API 31
+ See https://developer.android.com/about/versions/12/backup-restore
+-->
+<full-backup-content>
+ <exclude domain="sharedpref" path="faces.xml"/>
+</full-backup-content>
\ No newline at end of file
diff --git a/exampleApp/src/main/res/xml/data_extraction_rules.xml b/exampleApp/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..3fc20df
--- /dev/null
+++ b/exampleApp/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Sample data extraction rules file; uncomment and customize as necessary.
+ See https://developer.android.com/about/versions/12/backup-restore#xml-changes
+ for details.
+-->
+<data-extraction-rules>
+ <cloud-backup>
+ <exclude domain="sharedpref" path="faces.xml" />
+ </cloud-backup>
+ <device-transfer>
+ <exclude domain="sharedpref" path="faces.xml" />
+ </device-transfer>
+</data-extraction-rules>
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 72e3b45..d3d6890 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -13,5 +13,6 @@
}
}
rootProject.name = "FaceDetect"
-include ':app'
-include ':FaceShared'
\ No newline at end of file
+include ':FaceShared'
+include ':exampleApp'
+include ':app'
\ No newline at end of file