Compare commits
10 Commits
dev_v3.13.
...
dev_3.16.3
| Author | SHA1 | Date |
|---|---|---|
|
|
19dab0179d | 1 year ago |
|
|
7fbcee856a | 1 year ago |
|
|
caa3c75bae | 1 year ago |
|
|
f14a8af317 | 1 year ago |
|
|
cc504f8f6b | 1 year ago |
|
|
d5d805169a | 1 year ago |
|
|
89ed61d8f2 | 1 year ago |
|
|
7a1690e4ec | 1 year ago |
|
|
53996bdb39 | 1 year ago |
|
|
c0af7070b1 | 1 year ago |
@ -1,81 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.cloud.diplomaticquarterapp.whatsapp;
|
|
||||||
|
|
||||||
import static java.sql.DriverManager.println;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.ContextWrapper;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.pm.Signature;
|
|
||||||
import android.util.Base64;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class AppSignatureRetriever {
|
|
||||||
|
|
||||||
private static final String HASH_TYPE = "SHA-256";
|
|
||||||
public static final int NUM_HASHED_BYTES = 9;
|
|
||||||
public static final int NUM_BASE64_CHAR = 11;
|
|
||||||
|
|
||||||
|
|
||||||
public void logSignatures(Context context) {
|
|
||||||
Collection<String> appSignatures = getAppSignatures(context);
|
|
||||||
appSignatures.forEach(signature -> println("Signature: " + signature));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all the app signatures for the current package.
|
|
||||||
*
|
|
||||||
* @return signatures for current app
|
|
||||||
*/
|
|
||||||
public Collection<String> getAppSignatures(Context context) {
|
|
||||||
try {
|
|
||||||
// Get all package signatures for the current package
|
|
||||||
String packageName = context.getPackageName();
|
|
||||||
println("Package name: " + packageName);
|
|
||||||
PackageManager packageManager = context.getPackageManager();
|
|
||||||
Signature[] signatures = packageManager.getPackageInfo(packageName,
|
|
||||||
PackageManager.GET_SIGNATURES).signatures;
|
|
||||||
|
|
||||||
// For each signature create a compatible hash
|
|
||||||
Collection<String> appCodes = Arrays.stream(signatures)
|
|
||||||
.map(signature -> hash(packageName, signature.toCharsString()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
return appCodes;
|
|
||||||
} catch (PackageManager.NameNotFoundException e) {
|
|
||||||
println("Unable to find package to obtain hash.");
|
|
||||||
throw new RuntimeException("Unable to find package to obtain hash.", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private String hash(String packageName, String signature) {
|
|
||||||
String appInfo = packageName + " " + signature;
|
|
||||||
try {
|
|
||||||
MessageDigest messageDigest = MessageDigest.getInstance(HASH_TYPE);
|
|
||||||
messageDigest.update(appInfo.getBytes(StandardCharsets.UTF_8));
|
|
||||||
byte[] hashSignature = messageDigest.digest();
|
|
||||||
|
|
||||||
// truncated into NUM_HASHED_BYTES
|
|
||||||
hashSignature = Arrays.copyOfRange(hashSignature, 0, NUM_HASHED_BYTES);
|
|
||||||
// encode into Base64
|
|
||||||
String base64Hash = Base64.encodeToString(hashSignature, Base64.NO_PADDING | Base64.NO_WRAP);
|
|
||||||
base64Hash = base64Hash.substring(0, NUM_BASE64_CHAR);
|
|
||||||
|
|
||||||
println(String.format("pkg: %s -- hash: %s", packageName, base64Hash));
|
|
||||||
return base64Hash;
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
throw new RuntimeException("Unable to generate hash for application", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
|
|
||||||
package com.cloud.diplomaticquarterapp.whatsapp
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import com.whatsapp.otp.android.sdk.WhatsAppOtpHandler
|
|
||||||
import com.whatsapp.otp.android.sdk.WhatsAppOtpIncomingIntentHandler
|
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
|
|
||||||
object WhatsApp {
|
|
||||||
val whatsAppOtpHandler = WhatsAppOtpHandler()
|
|
||||||
inline fun handleOTP ( intent: Intent, crossinline validateOTP:(code: String )-> Unit) =
|
|
||||||
WhatsAppOtpIncomingIntentHandler().processOtpCode(
|
|
||||||
intent,
|
|
||||||
// call your function to validate
|
|
||||||
{code -> validateOTP(code) },
|
|
||||||
{error,exception->
|
|
||||||
println("the error is ${error.name}")
|
|
||||||
println("the exception stacktrace is ${exception.message}")
|
|
||||||
println("the exception is cause ${exception.cause}")
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
fun performHandShake(context : WeakReference<Context>) = whatsAppOtpHandler.sendOtpIntentToWhatsApp(context.get()!!)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun isWhatsAppInstalled(context : WeakReference<Context>) : Boolean = whatsAppOtpHandler.isWhatsAppInstalled(context.get()!!)
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
package com.ejada.hmg
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import com.cloud.diplomaticquarterapp.whatsapp.WhatsApp
|
|
||||||
import com.cloud.diplomaticquarterapp.whatsapp.WhatsAppOtpPlatformBridge
|
|
||||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
|
||||||
|
|
||||||
class WhatsAppCodeActivity : FlutterFragmentActivity() {
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
WhatsApp.handleOTP(intent){code ->
|
|
||||||
WhatsAppOtpPlatformBridge.result?.success(code);
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
package com.cloud.diplomaticquarterapp.whatsapp
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import com.cloud.diplomaticquarterapp.penguin.PenguinView
|
|
||||||
import com.ejada.hmg.MainActivity
|
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
|
||||||
import io.flutter.plugin.common.MethodCall
|
|
||||||
import io.flutter.plugin.common.MethodChannel
|
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
|
|
||||||
class WhatsAppOtpPlatformBridge(
|
|
||||||
private var flutterEngine: FlutterEngine,
|
|
||||||
private var mainActivity: MainActivity
|
|
||||||
) {
|
|
||||||
|
|
||||||
|
|
||||||
private lateinit var channel: MethodChannel
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val CHANNEL = "whats_app_otp"
|
|
||||||
var result: MethodChannel.Result? = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invoke() {
|
|
||||||
channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
|
|
||||||
channel.setMethodCallHandler { call: MethodCall, result: MethodChannel.Result ->
|
|
||||||
when (call.method) {
|
|
||||||
"isWhatsAppInstalled" -> {
|
|
||||||
val isAppInstalled =
|
|
||||||
WhatsApp.isWhatsAppInstalled(WeakReference(mainActivity))
|
|
||||||
result.success(isAppInstalled)
|
|
||||||
}
|
|
||||||
|
|
||||||
"performHandShake" -> {
|
|
||||||
WhatsApp.performHandShake(WeakReference(mainActivity))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
"startListening" -> {
|
|
||||||
WhatsAppOtpPlatformBridge.result = result
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
result.notImplemented()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/main"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".WhatsAppCodeActivity">
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
<svg id="route" xmlns="http://www.w3.org/2000/svg" width="27.521" height="27.521" viewBox="0 0 27.521 27.521">
|
|
||||||
<path id="Path_5246" data-name="Path 5246" d="M365.087,11.9a.806.806,0,0,0,1.457,0l2.916-6.138a4.034,4.034,0,1,0-7.288,0Zm.728-8.678a.806.806,0,1,1-.806.806A.806.806,0,0,1,365.815,3.225Z" transform="translate(-342.332)" fill="#28323a"/>
|
|
||||||
<ellipse id="Ellipse_225" data-name="Ellipse 225" cx="1.5" cy="1" rx="1.5" ry="1" transform="translate(14 21.522)" fill="#28323a"/>
|
|
||||||
<path id="Path_5247" data-name="Path 5247" d="M175.628,205.644a2.016,2.016,0,0,1,0-4.031h8.315L183.177,200h-7.549a3.628,3.628,0,0,0,0,7.257h4.537a3.235,3.235,0,0,1,0-1.613Z" transform="translate(-162.754 -189.249)" fill="#28323a"/>
|
|
||||||
<circle id="Ellipse_226" data-name="Ellipse 226" cx="1.5" cy="1.5" r="1.5" transform="translate(19 15.522)" fill="#28323a"/>
|
|
||||||
<path id="Path_5248" data-name="Path 5248" d="M350.39,305h-.183a3.236,3.236,0,0,1,0,1.613h.183a2.069,2.069,0,0,1,0,4.139H345.1a3.236,3.236,0,0,1,0,1.613h5.289a3.682,3.682,0,0,0,0-7.364Z" transform="translate(-326.551 -288.605)" fill="#28323a"/>
|
|
||||||
<path id="Path_5249" data-name="Path 5249" d="M167.02,412l.706,1.613h2.621a3.235,3.235,0,0,1,0-1.613Z" transform="translate(-158.042 -389.854)" fill="#28323a"/>
|
|
||||||
<path id="Path_5250" data-name="Path 5250" d="M5.311,322.415a.806.806,0,0,0-1.477,0l-3.763,8.6a.806.806,0,0,0,1.139,1.023l3.363-1.922,3.363,1.922a.806.806,0,0,0,1.139-1.023Z" transform="translate(0 -304.627)" fill="#28323a"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1,3 +0,0 @@
|
|||||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M27.2568 32L25.1631 30.6091C27.9468 26.2666 29.4463 21.2154 29.4998 15.9999C29.4463 10.7844 27.9468 5.73315 25.1631 1.39067L27.2568 0C30.304 4.75302 31.9441 10.2814 31.9998 15.9875L32 16.0001L31.9998 16.0128C31.9441 21.7186 30.304 27.247 27.2568 32ZM25.0625 16.0165L25.0627 15.9999L25.0625 15.983C25.0051 11.5525 23.6995 7.27342 21.2869 3.60788L19.2097 5.02439C21.3494 8.27467 22.5085 12.0694 22.5625 15.9999C22.5085 19.9301 21.3494 23.7248 19.2097 26.9751L21.2869 28.3916C23.6995 24.7263 25.0051 20.447 25.0625 16.0165ZM16.7007 22.9666C17.719 20.7477 18.2334 18.3302 18.188 15.9748C18.1284 12.8761 17.1565 9.86638 15.3779 7.27168L13.3276 8.72795C14.822 10.9081 15.6384 13.4313 15.6885 16.0247C15.7163 17.4783 15.4875 18.9644 15.0205 20.3943L1.56543 9.86414L0.925293 11.3547C0.887451 11.4427 0 13.5424 0 16.4452C0 19.4569 1.69165 22.1483 1.76367 22.2614L3.86157 20.8772C3.8479 20.8558 2.5 18.6766 2.5 16.4452C2.5 15.5068 2.61401 14.6693 2.75073 14.0018L16.0413 24.4033L16.7007 22.9666Z" fill="#2E303A"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1124.14 1256.39">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.cls-1 {
|
|
||||||
fill: #231f20;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<path class="cls-1" d="M699.62,1113.02h0c-20.06,44.48-33.32,92.75-38.4,143.37l424.51-90.24c20.06-44.47,33.31-92.75,38.4-143.37l-424.51,90.24Z"/>
|
|
||||||
<path class="cls-1" d="M1085.73,895.8c20.06-44.47,33.32-92.75,38.4-143.37l-330.68,70.33v-135.2l292.27-62.11c20.06-44.47,33.32-92.75,38.4-143.37l-330.68,70.27V66.13c-50.67,28.45-95.67,66.32-132.25,110.99v403.35l-132.25,28.11V0c-50.67,28.44-95.67,66.32-132.25,110.99v525.69l-295.91,62.88c-20.06,44.47-33.33,92.75-38.42,143.37l334.33-71.05v170.26l-358.3,76.14c-20.06,44.47-33.32,92.75-38.4,143.37l375.04-79.7c30.53-6.35,56.77-24.4,73.83-49.24l68.78-101.97v-.02c7.14-10.55,11.3-23.27,11.3-36.97v-149.98l132.25-28.11v270.4l424.53-90.28Z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 923 B |
@ -1,5 +0,0 @@
|
|||||||
<svg id="Download" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path
|
|
||||||
class="color000000 svgShape"
|
|
||||||
d="M1344 1344q0-26-19-45t-45-19-45 19-19 45 19 45 45 19 45-19 19-45zm256 0q0-26-19-45t-45-19-45 19-19 45 19 45 45 19 45-19 19-45zm128-224v320q0 40-28 68t-68 28H160q-40 0-68-28t-28-68v-320q0-40 28-68t68-28h465l135 136q58 56 136 56t136-56l136-136h464q40 0 68 28t28 68zm-325-569q17 41-14 70l-448 448q-18 19-45 19t-45-19L403 621q-31-29-14-70 17-39 59-39h256V64q0-26 19-45t45-19h256q26 0 45 19t19 45v448h256q42 0 59 39z" fill="#ffffff"></path>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 585 B |
@ -1,3 +0,0 @@
|
|||||||
<svg width="22" height="29" viewBox="0 0 22 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M15.5498 11.4402C17.4647 9.69433 20.0542 7.33316 20.2903 1.69231H22V0H0V1.69231H1.70974C1.94576 7.33316 4.53533 9.69433 6.45023 11.4402C7.73475 12.6113 8.46154 13.3281 8.46154 14.441C8.46154 15.5539 7.73475 16.2707 6.45023 17.4419C4.53533 19.1877 1.94576 21.5489 1.70974 27.1897H0V28.8821H22V27.1897H20.2903C20.0542 21.5489 17.4647 19.1877 15.5498 17.4419C14.2653 16.2707 13.5385 15.5539 13.5385 14.441C13.5385 13.3281 14.2653 12.6113 15.5498 11.4402ZM10.1538 20.7837C9.8925 20.8376 9.63431 20.9161 9.38232 21.0203L4.19952 23.165C5.0459 21.0129 6.40911 19.7695 7.5904 18.6925C8.9087 17.4906 10.1538 16.3553 10.1538 14.441V20.7837ZM14.4096 18.6925C15.5909 19.7695 16.954 21.0129 17.8005 23.1649L12.6177 21.0203C12.3657 20.916 12.1075 20.8376 11.8462 20.7836V14.441C11.8462 16.3553 13.0913 17.4906 14.4096 18.6925ZM4.16979 5.64103C3.76065 4.57493 3.47809 3.2877 3.40413 1.69231H18.5958C18.5219 3.2877 18.2394 4.57493 17.8302 5.64103H4.16979Z" fill="#2E303A"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.0 KiB |
@ -1,11 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="31" height="31" viewBox="0 0 31 31">
|
|
||||||
<g id="route" transform="translate(0 0)">
|
|
||||||
<path id="Path_5246" data-name="Path 5246" d="M365.505,13.407a.908.908,0,0,0,1.641,0l3.284-6.914a4.544,4.544,0,1,0-8.209,0Zm.82-9.774a.908.908,0,1,1-.908.908A.908.908,0,0,1,366.326,3.633Z" transform="translate(-339.874)" fill="#da4a54"/>
|
|
||||||
<circle id="Ellipse_225" data-name="Ellipse 225" cx="1.5" cy="1.5" r="1.5" transform="translate(16 24)" fill="#2e303a"/>
|
|
||||||
<path id="Path_5247" data-name="Path 5247" d="M176.087,206.357a2.27,2.27,0,0,1,0-4.541h9.366L184.59,200h-8.5a4.087,4.087,0,0,0,0,8.174h5.11a3.644,3.644,0,0,1,0-1.816Z" transform="translate(-161.586 -187.891)" fill="#2e303a"/>
|
|
||||||
<circle id="Ellipse_226" data-name="Ellipse 226" cx="2" cy="2" r="2" transform="translate(21 17)" fill="#2e303a"/>
|
|
||||||
<path id="Path_5248" data-name="Path 5248" d="M351.059,305h-.206a3.644,3.644,0,0,1,0,1.816h.206a2.331,2.331,0,0,1,0,4.662H345.1a3.645,3.645,0,0,1,0,1.816h5.958a4.147,4.147,0,0,0,0-8.295Z" transform="translate(-324.206 -286.533)" fill="#2e303a"/>
|
|
||||||
<path id="Path_5249" data-name="Path 5249" d="M167.02,412l.795,1.816h2.952a3.644,3.644,0,0,1,0-1.816Z" transform="translate(-156.907 -387.055)" fill="#2e303a"/>
|
|
||||||
<path id="Path_5250" data-name="Path 5250" d="M5.982,322.476a.908.908,0,0,0-1.664,0L.08,332.164a.908.908,0,0,0,1.283,1.153l3.788-2.164,3.788,2.164a.908.908,0,0,0,1.283-1.153Z" transform="translate(0 -302.44)" fill="#2e303a"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1,3 +0,0 @@
|
|||||||
<svg width="23" height="29" viewBox="0 0 23 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M19.2821 9.01172C18.9238 9.01172 18.5814 9.08629 18.2671 9.22183C17.9481 7.9542 16.8772 7.01953 15.6088 7.01953C14.9039 7.01953 14.2599 7.3082 13.7722 7.78227C13.2844 7.3082 12.6405 7.01953 11.9356 7.01953C11.6137 7.01953 11.3046 7.0797 11.0173 7.1902V3.96484C11.0173 2.31711 9.78138 0.976562 8.2623 0.976562C6.74323 0.976562 5.50736 2.31711 5.50736 3.96484V12.7547C5.06406 12.2739 4.70028 11.8793 4.70028 11.8793C3.68187 10.7746 2.03312 10.7115 0.9489 11.7334C-0.14065 12.7546 -0.316355 14.5345 0.548882 15.7856C0.558678 15.7998 0.568841 15.8136 0.57937 15.8272L4.66385 21.1185C5.1074 21.6931 5.74741 22.0491 6.31205 22.3632C6.72296 22.5918 7.34399 22.9373 7.34399 23.1565V26.0117C7.34399 27.6595 8.57986 29 10.0989 29H17.4455C18.9645 29 20.2004 27.6595 20.2004 26.0117V23.9531C20.2004 23.5591 20.5492 23.0843 20.9185 22.5818C21.4428 21.8683 22.037 21.0596 22.037 19.9688V12C22.037 10.3523 20.8012 9.01172 19.2821 9.01172ZM19.4823 21.3401C18.958 22.0536 18.3638 22.8623 18.3638 23.9531V26.0117C18.3638 26.561 17.9518 27.0078 17.4455 27.0078H10.0989C9.59258 27.0078 9.18062 26.561 9.18062 26.0117V23.1564C9.18062 21.72 8.00707 21.0672 7.1501 20.5904C6.72064 20.3515 6.2766 20.1046 6.072 19.8395L2.006 14.572C1.73131 14.1561 1.79259 13.5755 2.15165 13.2389C2.51304 12.8982 3.06213 12.9197 3.4016 13.2879C3.4016 13.2879 5.62662 15.7015 5.77685 15.8641C6.35294 16.4878 7.34393 16.0456 7.34393 15.1591V3.96484C7.34393 3.4156 7.75588 2.96875 8.26224 2.96875C8.7686 2.96875 9.18056 3.4156 9.18056 3.96484V12C9.18056 12.5501 9.59172 12.9961 10.0989 12.9961C10.606 12.9961 11.0172 12.5501 11.0172 12V10.0078C11.0172 9.45857 11.4291 9.01172 11.9355 9.01172C12.4419 9.01172 12.8538 9.45857 12.8538 10.0078V12C12.8538 12.5501 13.265 12.9961 13.7721 12.9961C14.2793 12.9961 14.6905 12.5501 14.6905 12V10.0078C14.6905 9.45857 15.1024 9.01172 15.6088 9.01172C16.1151 9.01172 16.5271 9.45857 16.5271 10.0078V12C16.5271 12.5501 16.9382 12.9961 17.4454 12.9961C17.9526 12.9961 18.3637 12.5501 18.3637 12C18.3637 11.4508 18.7757 11.0039 19.282 11.0039C19.7884 11.0039 20.2003 11.4508 20.2003 12V19.9688H20.2004C20.2004 20.3628 19.8516 20.8375 19.4823 21.3401Z" fill="#2E303A"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.2 KiB |
@ -1,6 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="31" height="31" viewBox="0 0 31 31">
|
|
||||||
<g id="Group_8325" data-name="Group 8325" transform="translate(-75 -236)">
|
|
||||||
<circle id="Ellipse_223" data-name="Ellipse 223" cx="15.5" cy="15.5" r="15.5" transform="translate(75 236)" fill="#359846"/>
|
|
||||||
<path id="hour-glass" d="M8.73,10.811a1.46,1.46,0,0,0,0-2.122L8.647,8.61C7.085,7.119,5.878,5.968,5.515,3.556A1.413,1.413,0,0,1,6.992,2h7.091A1.413,1.413,0,0,1,15.56,3.556C15.2,5.968,13.99,7.119,12.429,8.61l-.083.079a1.46,1.46,0,0,0,0,2.122l.083.079c1.562,1.491,2.768,2.642,3.132,5.054A1.413,1.413,0,0,1,14.083,17.5H6.992a1.413,1.413,0,0,1-1.477-1.556c.364-2.412,1.57-3.563,3.132-5.054Zm.529,1.316-.006,0c-1.19,1.064-2.076,1.856-2.191,3.816a.363.363,0,0,0,.13.3.382.382,0,0,0,.247.09h6.194a.382.382,0,0,0,.282-.122.361.361,0,0,0,.1-.268c-.115-1.959-1-2.752-2.191-3.816-.315-.281-.651-.582-1-.928a.4.4,0,0,0-.569,0c-.347.344-.681.643-.993.923Z" transform="translate(80.35 241.75)" fill="#fff" fill-rule="evenodd"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1021 B |
@ -1,70 +0,0 @@
|
|||||||
// import 'package:flutter/material.dart';
|
|
||||||
// import 'package:vital_sign_camera/vital_sign_camera.dart';
|
|
||||||
//
|
|
||||||
// class VitalSigns extends StatefulWidget {
|
|
||||||
// const VitalSigns({super.key});
|
|
||||||
//
|
|
||||||
// @override
|
|
||||||
// State<VitalSigns> createState() => _VitalSignState();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// class _VitalSignState extends State<VitalSigns> {
|
|
||||||
// late final VitalSignCameraController _vitalSignCameraController;
|
|
||||||
// late Future<CameraDevice?> cameraDevice;
|
|
||||||
//
|
|
||||||
// @override
|
|
||||||
// void initState() {
|
|
||||||
// super.initState();
|
|
||||||
// cameraDevice = getFrontCamera();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// double? _heartRate;
|
|
||||||
//
|
|
||||||
// Future<CameraDevice?> getFrontCamera() async {
|
|
||||||
// if (CameraPermissionStatus.authorized != await requestCameraPermission()) {
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
// return queryCameraDevice(CameraPosition.front);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @override
|
|
||||||
// Widget build(BuildContext context) {
|
|
||||||
// return Scaffold(
|
|
||||||
// body: Stack(children: [
|
|
||||||
// VitalSignCamera(
|
|
||||||
// onCreated: _onVitalSignCameraCreated,
|
|
||||||
// isActive: true,
|
|
||||||
// userInfo: UserInfo(
|
|
||||||
// age: 30, gender: Gender.male, userId: '__YOUR_USER_ID__'),
|
|
||||||
// config: VitalSignCameraConfig(apiKey: '__YOUR_API_KEY__'),
|
|
||||||
// device: cameraDevice,
|
|
||||||
// onVideoFrameProcessed: _onVideoFrameProcessed),
|
|
||||||
// Center(
|
|
||||||
// child: Column(
|
|
||||||
// mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
// children: [
|
|
||||||
// ElevatedButton(
|
|
||||||
// onPressed: () {
|
|
||||||
// setState(() {
|
|
||||||
// _vitalSignCameraController.startScanning();
|
|
||||||
// });
|
|
||||||
// },
|
|
||||||
// child: const Text('start')),
|
|
||||||
// Text('Heart rate: $_heartRate'),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ]),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// void _onVideoFrameProcessed(VideoFrameProcessedEvent event) {
|
|
||||||
// // setState(() {
|
|
||||||
// // _heartRate = event.healthResult?.health?.vitalSigns.heartRate;
|
|
||||||
// // });
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// void _onVitalSignCameraCreated(VitalSignCameraController controller) {
|
|
||||||
// _vitalSignCameraController = controller;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@ -1 +0,0 @@
|
|||||||
TODO: Add your license here.
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
include: package:flutter_lints/flutter.yaml
|
|
||||||
|
|
||||||
# Additional information about this file can be found at
|
|
||||||
# https://dart.dev/guides/language/analysis-options
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
*.iml
|
|
||||||
.gradle
|
|
||||||
/local.properties
|
|
||||||
/.idea/workspace.xml
|
|
||||||
/.idea/libraries
|
|
||||||
.DS_Store
|
|
||||||
/build
|
|
||||||
/captures
|
|
||||||
.cxx
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
group 'ai.panoptic.vital_sign_camera'
|
|
||||||
version '1.0-SNAPSHOT'
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
ext.kotlin_version = '1.5.20'
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
classpath 'com.android.tools.build:gradle:7.1.2'
|
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allprojects {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: 'com.android.library'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdkVersion 34
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = '1.8'
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
main.java.srcDirs += 'src/main/kotlin'
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdkVersion 24
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
|
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
|
||||||
implementation 'androidx.camera:camera-core:1.0.2'
|
|
||||||
implementation 'com.google.mlkit:face-detection:16.1.2'
|
|
||||||
implementation 'com.android.volley:volley:1.2.1'
|
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
||||||
implementation project(":vital-sign-engine")
|
|
||||||
|
|
||||||
implementation "androidx.camera:camera-core:1.1.0-beta02"
|
|
||||||
implementation "androidx.camera:camera-camera2:1.1.0-beta02"
|
|
||||||
implementation "androidx.camera:camera-lifecycle:1.1.0-beta02"
|
|
||||||
implementation "androidx.camera:camera-video:1.1.0-beta02"
|
|
||||||
|
|
||||||
implementation "androidx.camera:camera-view:1.1.0-beta02"
|
|
||||||
implementation "androidx.camera:camera-extensions:1.1.0-beta02"
|
|
||||||
|
|
||||||
implementation "androidx.exifinterface:exifinterface:1.3.3"
|
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.5.2"
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
|
||||||
distributionPath=wrapper/dists
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
|
||||||
@ -1,234 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
#
|
|
||||||
# Copyright © 2015-2021 the original authors.
|
|
||||||
#
|
|
||||||
# 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
|
|
||||||
#
|
|
||||||
# https://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.
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Gradle start up script for POSIX generated by Gradle.
|
|
||||||
#
|
|
||||||
# Important for running:
|
|
||||||
#
|
|
||||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
|
||||||
# noncompliant, but you have some other compliant shell such as ksh or
|
|
||||||
# bash, then to run this script, type that shell name before the whole
|
|
||||||
# command line, like:
|
|
||||||
#
|
|
||||||
# ksh Gradle
|
|
||||||
#
|
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
|
||||||
# requires all of these POSIX shell features:
|
|
||||||
# * functions;
|
|
||||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
|
||||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
|
||||||
# * compound commands having a testable exit status, especially «case»;
|
|
||||||
# * various built-in commands including «command», «set», and «ulimit».
|
|
||||||
#
|
|
||||||
# Important for patching:
|
|
||||||
#
|
|
||||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
|
||||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
|
||||||
#
|
|
||||||
# The "traditional" practice of packing multiple parameters into a
|
|
||||||
# space-separated string is a well documented source of bugs and security
|
|
||||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
|
||||||
# options in "$@", and eventually passing that to Java.
|
|
||||||
#
|
|
||||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
|
||||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
|
||||||
# see the in-line comments for details.
|
|
||||||
#
|
|
||||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
|
||||||
# Darwin, MinGW, and NonStop.
|
|
||||||
#
|
|
||||||
# (3) This script is generated from the Groovy template
|
|
||||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
|
||||||
# within the Gradle project.
|
|
||||||
#
|
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
|
||||||
app_path=$0
|
|
||||||
|
|
||||||
# Need this for daisy-chained symlinks.
|
|
||||||
while
|
|
||||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
|
||||||
[ -h "$app_path" ]
|
|
||||||
do
|
|
||||||
ls=$( ls -ld "$app_path" )
|
|
||||||
link=${ls#*' -> '}
|
|
||||||
case $link in #(
|
|
||||||
/*) app_path=$link ;; #(
|
|
||||||
*) app_path=$APP_HOME$link ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
|
||||||
|
|
||||||
APP_NAME="Gradle"
|
|
||||||
APP_BASE_NAME=${0##*/}
|
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
|
||||||
MAX_FD=maximum
|
|
||||||
|
|
||||||
warn () {
|
|
||||||
echo "$*"
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
die () {
|
|
||||||
echo
|
|
||||||
echo "$*"
|
|
||||||
echo
|
|
||||||
exit 1
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
|
||||||
cygwin=false
|
|
||||||
msys=false
|
|
||||||
darwin=false
|
|
||||||
nonstop=false
|
|
||||||
case "$( uname )" in #(
|
|
||||||
CYGWIN* ) cygwin=true ;; #(
|
|
||||||
Darwin* ) darwin=true ;; #(
|
|
||||||
MSYS* | MINGW* ) msys=true ;; #(
|
|
||||||
NONSTOP* ) nonstop=true ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
|
||||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
|
||||||
else
|
|
||||||
JAVACMD=$JAVA_HOME/bin/java
|
|
||||||
fi
|
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
JAVACMD=java
|
|
||||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|
||||||
case $MAX_FD in #(
|
|
||||||
max*)
|
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
|
||||||
warn "Could not query maximum file descriptor limit"
|
|
||||||
esac
|
|
||||||
case $MAX_FD in #(
|
|
||||||
'' | soft) :;; #(
|
|
||||||
*)
|
|
||||||
ulimit -n "$MAX_FD" ||
|
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Collect all arguments for the java command, stacking in reverse order:
|
|
||||||
# * args from the command line
|
|
||||||
# * the main class name
|
|
||||||
# * -classpath
|
|
||||||
# * -D...appname settings
|
|
||||||
# * --module-path (only if needed)
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
|
||||||
if "$cygwin" || "$msys" ; then
|
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
|
||||||
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
for arg do
|
|
||||||
if
|
|
||||||
case $arg in #(
|
|
||||||
-*) false ;; # don't mess with options #(
|
|
||||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
|
||||||
[ -e "$t" ] ;; #(
|
|
||||||
*) false ;;
|
|
||||||
esac
|
|
||||||
then
|
|
||||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
|
||||||
fi
|
|
||||||
# Roll the args list around exactly as many times as the number of
|
|
||||||
# args, so each arg winds up back in the position where it started, but
|
|
||||||
# possibly modified.
|
|
||||||
#
|
|
||||||
# NB: a `for` loop captures its iteration list before it begins, so
|
|
||||||
# changing the positional parameters here affects neither the number of
|
|
||||||
# iterations, nor the values presented in `arg`.
|
|
||||||
shift # remove old arg
|
|
||||||
set -- "$@" "$arg" # push replacement arg
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Collect all arguments for the java command;
|
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
|
||||||
# shell script including quotes and variable substitutions, so put them in
|
|
||||||
# double quotes to make sure that they get re-expanded; and
|
|
||||||
# * put everything else in single quotes, so that it's not re-expanded.
|
|
||||||
|
|
||||||
set -- \
|
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
|
||||||
-classpath "$CLASSPATH" \
|
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
|
||||||
"$@"
|
|
||||||
|
|
||||||
# Use "xargs" to parse quoted args.
|
|
||||||
#
|
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
|
||||||
#
|
|
||||||
# In Bash we could simply go:
|
|
||||||
#
|
|
||||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
|
||||||
# set -- "${ARGS[@]}" "$@"
|
|
||||||
#
|
|
||||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
|
||||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
|
||||||
# character that might be a shell metacharacter, then use eval to reverse
|
|
||||||
# that process (while maintaining the separation between arguments), and wrap
|
|
||||||
# the whole thing up as a single "set" statement.
|
|
||||||
#
|
|
||||||
# This will of course break if any of these variables contains a newline or
|
|
||||||
# an unmatched quote.
|
|
||||||
#
|
|
||||||
|
|
||||||
eval "set -- $(
|
|
||||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
|
||||||
xargs -n1 |
|
|
||||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
|
||||||
tr '\n' ' '
|
|
||||||
)" '"$@"'
|
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
@rem
|
|
||||||
@rem Copyright 2015 the original author or authors.
|
|
||||||
@rem
|
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
@rem you may not use this file except in compliance with the License.
|
|
||||||
@rem You may obtain a copy of the License at
|
|
||||||
@rem
|
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
@rem
|
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
@rem See the License for the specific language governing permissions and
|
|
||||||
@rem limitations under the License.
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%" == "" @echo off
|
|
||||||
@rem ##########################################################################
|
|
||||||
@rem
|
|
||||||
@rem Gradle startup script for Windows
|
|
||||||
@rem
|
|
||||||
@rem ##########################################################################
|
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
|
||||||
if "%DIRNAME%" == "" set DIRNAME=.
|
|
||||||
set APP_BASE_NAME=%~n0
|
|
||||||
set APP_HOME=%DIRNAME%
|
|
||||||
|
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
|
||||||
|
|
||||||
@rem Find java.exe
|
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
|
||||||
if "%ERRORLEVEL%" == "0" goto execute
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
echo.
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
echo location of your Java installation.
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
|
||||||
echo.
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
echo location of your Java installation.
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:execute
|
|
||||||
@rem Setup the command line
|
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
|
||||||
|
|
||||||
:end
|
|
||||||
@rem End local scope for the variables with windows NT shell
|
|
||||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
|
||||||
|
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
|
||||||
exit /b 1
|
|
||||||
|
|
||||||
:mainEnd
|
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
|
||||||
|
|
||||||
:omega
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
configurations.maybeCreate("default")
|
|
||||||
artifacts.add("default", file('vital-sign-engine-debug.aar'))
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="ai.panoptic.vital_sign_camera">
|
|
||||||
</manifest>
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
package ai.panoptic.vital_sign_camera
|
|
||||||
|
|
||||||
import ai.panoptic.wvs.camera.*
|
|
||||||
import ai.panoptic.wvs.engine.Converter
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.view.View
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import io.flutter.plugin.common.BinaryMessenger
|
|
||||||
import io.flutter.plugin.common.EventChannel
|
|
||||||
import io.flutter.plugin.common.MethodCall
|
|
||||||
import io.flutter.plugin.common.MethodChannel
|
|
||||||
import io.flutter.plugin.platform.PlatformView
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
class FlutterVitalSignCamera internal constructor(
|
|
||||||
context: Context,
|
|
||||||
messenger: BinaryMessenger,
|
|
||||||
id: Int,
|
|
||||||
props: Map<String, Any>,
|
|
||||||
lifecycle: Lifecycle
|
|
||||||
) : PlatformView, MethodChannel.MethodCallHandler, EventChannel.StreamHandler, VitalSignCameraEventHandler {
|
|
||||||
|
|
||||||
private val camera: VitalSignCamera
|
|
||||||
private val methodChannel: MethodChannel
|
|
||||||
private var eventSink: EventChannel.EventSink? = null
|
|
||||||
private val mainThread = Handler(Looper.getMainLooper())
|
|
||||||
|
|
||||||
override fun getView(): View {
|
|
||||||
return camera
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
camera = VitalSignCamera(context, MLKitFaceDetector(), lifecycle, this)
|
|
||||||
|
|
||||||
methodChannel = MethodChannel(messenger, "ai.panoptic/channel/flutter_vital_sign_camera_$id")
|
|
||||||
methodChannel.setMethodCallHandler(this)
|
|
||||||
|
|
||||||
val eventChannel = EventChannel(messenger, "ai.panoptic/channel/flutter_vital_sign_camera_processed_frame_event_$id")
|
|
||||||
eventChannel.setStreamHandler(this)
|
|
||||||
|
|
||||||
camera.setProps(props)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMethodCall(methodCall: MethodCall, result: MethodChannel.Result) {
|
|
||||||
when (methodCall.method) {
|
|
||||||
"startScanning" -> {
|
|
||||||
camera.startStartScanning()
|
|
||||||
.thenAccept { result.success(null) }
|
|
||||||
.exceptionally {
|
|
||||||
result.error("-1", it.message, it.cause)
|
|
||||||
return@exceptionally null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"stopScanning" -> {
|
|
||||||
camera.stopScanning()
|
|
||||||
.thenAccept { result.success(null) }
|
|
||||||
.exceptionally {
|
|
||||||
result.error("-2", it.message, it.cause)
|
|
||||||
return@exceptionally null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"configure" -> {
|
|
||||||
val arguments = methodCall.arguments as? Map<String, Any>
|
|
||||||
arguments?.let {
|
|
||||||
camera.setProps(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
result.notImplemented()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun dispose() {
|
|
||||||
// camera.destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onVideoFrameProcessed(result: ai.panoptic.wvs.engine.ProcessFrameResult): kotlin.Unit {
|
|
||||||
// Already handled in onEvent.
|
|
||||||
// No need to handle here.
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onEvent(cameraView: VitalSignCamera, eventName: String, event: Map<String,Any>?) {
|
|
||||||
when(eventName) {
|
|
||||||
"videoFrameProcessed" -> {
|
|
||||||
eventSink?.let {
|
|
||||||
mainThread.post {
|
|
||||||
it.success(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
|
||||||
eventSink = events
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCancel(arguments: Any?) {
|
|
||||||
eventSink = null
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,165 +0,0 @@
|
|||||||
package ai.panoptic.vital_sign_camera;
|
|
||||||
|
|
||||||
import static com.google.mlkit.vision.face.FaceContour.FACE;
|
|
||||||
|
|
||||||
import android.graphics.PointF;
|
|
||||||
import android.media.Image;
|
|
||||||
import android.util.ArraySet;
|
|
||||||
|
|
||||||
import com.google.android.gms.tasks.Task;
|
|
||||||
import com.google.mlkit.vision.common.InputImage;
|
|
||||||
import com.google.mlkit.vision.face.Face;
|
|
||||||
import com.google.mlkit.vision.face.FaceContour;
|
|
||||||
import com.google.mlkit.vision.face.FaceDetection;
|
|
||||||
import com.google.mlkit.vision.face.FaceDetector;
|
|
||||||
import com.google.mlkit.vision.face.FaceDetectorOptions;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import ai.panoptic.wvs.engine.EyesOpenProbability;
|
|
||||||
import ai.panoptic.wvs.engine.FaceDetectorFeature;
|
|
||||||
import ai.panoptic.wvs.engine.HeadEulerAngle;
|
|
||||||
import ai.panoptic.wvs.engine.NormalizedFacebox;
|
|
||||||
import ai.panoptic.wvs.engine.VitalSignFaceDetector;
|
|
||||||
|
|
||||||
public class MLKitFaceDetector implements VitalSignFaceDetector {
|
|
||||||
|
|
||||||
ai.panoptic.wvs.engine.Face face = null;
|
|
||||||
|
|
||||||
FaceDetectorOptions options =
|
|
||||||
new FaceDetectorOptions.Builder()
|
|
||||||
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
|
|
||||||
.setContourMode(FaceDetectorOptions.CONTOUR_MODE_NONE)
|
|
||||||
.setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
|
|
||||||
.setMinFaceSize(0.15f)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
FaceDetector faceDetector = FaceDetection.getClient(options);
|
|
||||||
|
|
||||||
public ai.panoptic.wvs.engine.Face detectFace(Image mediaImage, int rotationDegrees) {
|
|
||||||
|
|
||||||
if (mediaImage == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
float width = mediaImage.getWidth();
|
|
||||||
float height = mediaImage.getHeight();
|
|
||||||
if (rotationDegrees == 270 || rotationDegrees == 90) {
|
|
||||||
width = mediaImage.getHeight();
|
|
||||||
height = mediaImage.getWidth();
|
|
||||||
}
|
|
||||||
final float _width = width;
|
|
||||||
final float _height = height;
|
|
||||||
InputImage image = InputImage.fromMediaImage(mediaImage, rotationDegrees);
|
|
||||||
|
|
||||||
Task<List<Face>> task = faceDetector.process(image);
|
|
||||||
|
|
||||||
task.addOnCompleteListener( result -> {
|
|
||||||
|
|
||||||
if (result.isCanceled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!result.isSuccessful()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!result.isComplete()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
List<Face> faces = result.getResult();
|
|
||||||
if (faces.isEmpty()) {
|
|
||||||
this.face = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Face face = faces.get(0);
|
|
||||||
this.face = createFace(face, _width, _height);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
this.face = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
return face;
|
|
||||||
}
|
|
||||||
|
|
||||||
static private ai.panoptic.wvs.engine.Face createFace(Face face, float _width, float _height) {
|
|
||||||
|
|
||||||
FaceContour contour = face.getContour(FACE);
|
|
||||||
|
|
||||||
NormalizedFacebox facebox = new NormalizedFacebox();
|
|
||||||
float minX = _width;
|
|
||||||
float minY = _height;
|
|
||||||
float maxX = 0;
|
|
||||||
float maxY = 0;
|
|
||||||
|
|
||||||
if (contour != null) {
|
|
||||||
|
|
||||||
for (PointF p : contour.getPoints()) {
|
|
||||||
if (p.x < minX) {
|
|
||||||
minX = p.x;
|
|
||||||
}
|
|
||||||
if (p.y < minY) {
|
|
||||||
minY = p.y;
|
|
||||||
}
|
|
||||||
if (p.x > maxX) {
|
|
||||||
maxX = p.x;
|
|
||||||
}
|
|
||||||
if (p.y > maxY) {
|
|
||||||
maxY = p.y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
minX = face.getBoundingBox().left;
|
|
||||||
minY = face.getBoundingBox().top;
|
|
||||||
maxX = face.getBoundingBox().right;
|
|
||||||
maxY = face.getBoundingBox().bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
facebox.xCenter = (minX + (maxX - minX) / 2.0f) / _width;
|
|
||||||
facebox.yCenter = (minY + (maxY - minY) / 2.0f) / _height;
|
|
||||||
facebox.width = (maxX - minX) / _width;
|
|
||||||
facebox.height = (maxY - minY) / _height;
|
|
||||||
|
|
||||||
ai.panoptic.wvs.engine.Face result = new ai.panoptic.wvs.engine.Face(facebox);
|
|
||||||
|
|
||||||
result.setEulerAngle(
|
|
||||||
new HeadEulerAngle(
|
|
||||||
face.getHeadEulerAngleX(),
|
|
||||||
face.getHeadEulerAngleY(),
|
|
||||||
face.getHeadEulerAngleY()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (face.getLeftEyeOpenProbability() != null && face.getRightEyeOpenProbability() != null) {
|
|
||||||
result.setEyesOpenProbability(
|
|
||||||
new EyesOpenProbability(
|
|
||||||
face.getLeftEyeOpenProbability(),
|
|
||||||
face.getRightEyeOpenProbability()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Set<FaceDetectorFeature> supportedFeatures() {
|
|
||||||
ArraySet<FaceDetectorFeature> features = new ArraySet<FaceDetectorFeature>();
|
|
||||||
features.add(FaceDetectorFeature.eyeOpenProbability);
|
|
||||||
features.add(FaceDetectorFeature.eulerAngle);
|
|
||||||
return features;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public NormalizedFacebox detect(Image mediaImage, int rotationDegrees) {
|
|
||||||
detectFace(mediaImage, rotationDegrees);
|
|
||||||
if (face == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return face.getFacebox();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,145 +0,0 @@
|
|||||||
package ai.panoptic.vital_sign_camera
|
|
||||||
|
|
||||||
import ai.panoptic.wvs.camera.availableCameraDevices
|
|
||||||
import android.Manifest
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.annotation.NonNull
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.*
|
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
|
||||||
import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter
|
|
||||||
import io.flutter.plugin.common.MethodCall
|
|
||||||
import io.flutter.plugin.common.MethodChannel
|
|
||||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
|
||||||
import io.flutter.plugin.common.MethodChannel.Result
|
|
||||||
import io.flutter.plugin.common.PluginRegistry
|
|
||||||
import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
/** VitalSignCameraPlugin */
|
|
||||||
class VitalSignCameraPlugin: FlutterPlugin, MethodCallHandler, ActivityAware,
|
|
||||||
RequestPermissionsResultListener {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TAG = "VitalSignCameraPlugin"
|
|
||||||
|
|
||||||
fun parsePermissionStatus(status: Int): String {
|
|
||||||
return when (status) {
|
|
||||||
PackageManager.PERMISSION_DENIED -> "denied"
|
|
||||||
PackageManager.PERMISSION_GRANTED -> "authorized"
|
|
||||||
else -> "not-determined"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The MethodChannel that will the communication between Flutter and native Android
|
|
||||||
///
|
|
||||||
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
|
|
||||||
/// when the Flutter Engine is detached from the Activity
|
|
||||||
private lateinit var channel : MethodChannel
|
|
||||||
private lateinit var pluginBinding: FlutterPlugin.FlutterPluginBinding
|
|
||||||
private lateinit var context: Context
|
|
||||||
private lateinit var activity: Activity
|
|
||||||
private lateinit var activityBinding:ActivityPluginBinding
|
|
||||||
private val coroutineScope = CoroutineScope(Dispatchers.Main)
|
|
||||||
private var requestCode = 1
|
|
||||||
private var permissionRequests = HashMap<Int, Result>()
|
|
||||||
|
|
||||||
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
|
||||||
|
|
||||||
Log.d("VitalSignCameraPlugin", "onAttachedToEngine")
|
|
||||||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "vital_sign_camera_plugin")
|
|
||||||
channel.setMethodCallHandler(this)
|
|
||||||
pluginBinding = flutterPluginBinding
|
|
||||||
context = flutterPluginBinding.applicationContext
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
|
|
||||||
channel.setMethodCallHandler(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
|
||||||
Log.d(TAG, "onAttachedToActivity")
|
|
||||||
|
|
||||||
val lifecycle: Lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding)
|
|
||||||
|
|
||||||
pluginBinding.platformViewRegistry.registerViewFactory(
|
|
||||||
"ai.panoptic/flutter_vital_sign_camera",
|
|
||||||
VitalSignCameraFactory(pluginBinding.binaryMessenger, lifecycle)
|
|
||||||
)
|
|
||||||
|
|
||||||
activity = binding.activity
|
|
||||||
activityBinding = binding
|
|
||||||
activityBinding.addRequestPermissionsResultListener(this)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetachedFromActivityForConfigChanges() {}
|
|
||||||
|
|
||||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {}
|
|
||||||
|
|
||||||
override fun onDetachedFromActivity() {
|
|
||||||
activityBinding.removeRequestPermissionsResultListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
|
|
||||||
|
|
||||||
when (call.method) {
|
|
||||||
"availableCameraDevices" -> {
|
|
||||||
coroutineScope.launch {
|
|
||||||
val devices = availableCameraDevices(context)
|
|
||||||
val returnValue = devices.map { it.map }
|
|
||||||
Log.d(TAG, "$returnValue")
|
|
||||||
result.success(returnValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"getCameraPermissionStatus" -> {
|
|
||||||
Log.d(TAG, "getCameraPermissionStatus")
|
|
||||||
val status =
|
|
||||||
parsePermissionStatus(
|
|
||||||
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA))
|
|
||||||
result.success(status)
|
|
||||||
}
|
|
||||||
"requestCameraPermission" -> {
|
|
||||||
Log.d(TAG, "requestCameraPermission")
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
|
||||||
// API 21 and below always grants permission on app install
|
|
||||||
result.success("authorized")
|
|
||||||
}
|
|
||||||
activity.requestPermissions(arrayOf(Manifest.permission.CAMERA), requestCode)
|
|
||||||
permissionRequests[requestCode] = result
|
|
||||||
requestCode += 1
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(
|
|
||||||
requestCode: Int,
|
|
||||||
permissions: Array<out String>,
|
|
||||||
grantResults: IntArray
|
|
||||||
): Boolean {
|
|
||||||
|
|
||||||
permissionRequests[requestCode]?.let {
|
|
||||||
val permissionStatus = if (grantResults.isNotEmpty()) grantResults[0] else PackageManager.PERMISSION_DENIED
|
|
||||||
it.success(parsePermissionStatus(permissionStatus))
|
|
||||||
permissionRequests.remove(requestCode)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,218 +0,0 @@
|
|||||||
//
|
|
||||||
// MLKitFaceDetector.swift
|
|
||||||
// VitalSignEngineCore
|
|
||||||
//
|
|
||||||
// Created by Eddie Lau on 24/8/2022.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import MLKitFaceDetection
|
|
||||||
import MLKitVision
|
|
||||||
import VitalSignEngineCore
|
|
||||||
|
|
||||||
public class MLKitFaceDetector : VitalSignFaceDetector {
|
|
||||||
public typealias Face = MLKitFace
|
|
||||||
|
|
||||||
public let supportedFeatures: Set<VitalSignEngineCore.FaceDetectorFeature>
|
|
||||||
= [.eyesOpenProbability, .eularAngle]
|
|
||||||
|
|
||||||
public init() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public let maxDetectionHeight:CGFloat = 240
|
|
||||||
let fps = Fps(name: "MLKitFaceDetector")
|
|
||||||
|
|
||||||
lazy var faceDetector:FaceDetector = {
|
|
||||||
let option = FaceDetectorOptions()
|
|
||||||
option.contourMode = .all
|
|
||||||
option.classificationMode = .all
|
|
||||||
option.landmarkMode = .none
|
|
||||||
option.performanceMode = .fast
|
|
||||||
return FaceDetector.faceDetector(options: option)
|
|
||||||
}()
|
|
||||||
|
|
||||||
var face:Face? {
|
|
||||||
didSet {
|
|
||||||
fps.tick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var frameRate: Double? { fps.lastReportedAverage }
|
|
||||||
|
|
||||||
public var maxConcurrency = 2
|
|
||||||
var jobCount = 0
|
|
||||||
|
|
||||||
public func detect(from frame: UIImage) -> Face? {
|
|
||||||
|
|
||||||
guard jobCount < maxConcurrency else {
|
|
||||||
return face
|
|
||||||
}
|
|
||||||
|
|
||||||
jobCount += 1
|
|
||||||
DispatchQueue.global().async {
|
|
||||||
|
|
||||||
let scaledFrame = self.scaled(videoFrame: frame)
|
|
||||||
let frameSize = scaledFrame.size
|
|
||||||
|
|
||||||
let image = VisionImage(image: scaledFrame)
|
|
||||||
image.orientation = image.orientation
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
|
|
||||||
self.faceDetector.process(image) { faces, error in
|
|
||||||
|
|
||||||
guard let face = faces?.first else {
|
|
||||||
self.face = nil
|
|
||||||
self.jobCount -= 1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.face = MLKitFace(face: face, frameSize: frameSize)
|
|
||||||
self.jobCount -= 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return face
|
|
||||||
}
|
|
||||||
|
|
||||||
func scaled(videoFrame frame:UIImage) -> UIImage {
|
|
||||||
var scale = CGFloat(1.0)
|
|
||||||
if frame.size.height > maxDetectionHeight {
|
|
||||||
scale = maxDetectionHeight / frame.size.height
|
|
||||||
}
|
|
||||||
let scaledScale = CGSize(width:frame.size.width * scale, height:frame.size.height * scale)
|
|
||||||
return frame.resizedImage(size: scaledScale)!
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct MLKitFaceLandmarks: FaceLandmarks {
|
|
||||||
public typealias ContourType = FaceContour
|
|
||||||
let mlkitFace:MLKitFaceDetection.Face
|
|
||||||
public let videoFrameSize: CGSize
|
|
||||||
init(mlkitFace: MLKitFaceDetection.Face, videoFrameSize: CGSize) {
|
|
||||||
self.mlkitFace = mlkitFace
|
|
||||||
self.videoFrameSize = videoFrameSize
|
|
||||||
}
|
|
||||||
public var faceOval: FaceContour? { mlkitFace.contour(ofType: .face) }
|
|
||||||
public func contour(of type: VitalSignEngineCore.FaceLandmarkType) -> FaceContour? {
|
|
||||||
guard let contourType = type.asContourType else { return nil }
|
|
||||||
return mlkitFace.contour(ofType: contourType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class MLKitFace : FaceProtocol {
|
|
||||||
public typealias ContourType = FaceContour
|
|
||||||
|
|
||||||
let mlkitFace:MLKitFaceDetection.Face
|
|
||||||
|
|
||||||
public let faceBox: NormalizedFaceBox
|
|
||||||
public let faceLandmarks: MLKitFaceLandmarks
|
|
||||||
public let eulerAngle: (x: CGFloat, y: CGFloat, z: CGFloat)?
|
|
||||||
public let eyesOpenProbability: (leftEye:CGFloat, rightEye:CGFloat)?
|
|
||||||
|
|
||||||
init?(face:MLKitFaceDetection.Face, frameSize:CGSize) {
|
|
||||||
|
|
||||||
self.mlkitFace = face
|
|
||||||
self.faceLandmarks = MLKitFaceLandmarks(mlkitFace: face, videoFrameSize: frameSize)
|
|
||||||
|
|
||||||
guard let faceContour = face.contour(ofType: FaceContourType.face) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let eyesOpenPropbability: (leftEye:CGFloat, rightEye:CGFloat)?
|
|
||||||
if face.hasLeftEyeOpenProbability && face.hasRightEyeOpenProbability {
|
|
||||||
eyesOpenPropbability = (face.leftEyeOpenProbability, face.rightEyeOpenProbability)
|
|
||||||
} else {
|
|
||||||
eyesOpenPropbability = nil
|
|
||||||
}
|
|
||||||
self.eyesOpenProbability = eyesOpenPropbability
|
|
||||||
|
|
||||||
let eulerAngle: (x:CGFloat, y:CGFloat, z:CGFloat)?
|
|
||||||
if face.hasHeadEulerAngleX && face.hasHeadEulerAngleY && face.hasHeadEulerAngleZ {
|
|
||||||
eulerAngle = (face.headEulerAngleX, face.headEulerAngleY, face.headEulerAngleZ)
|
|
||||||
} else {
|
|
||||||
eulerAngle = nil
|
|
||||||
}
|
|
||||||
self.eulerAngle = eulerAngle
|
|
||||||
|
|
||||||
self.faceBox = faceContour.toFacebox(frameSize: frameSize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate extension MLKitFaceDetection.FaceContour {
|
|
||||||
|
|
||||||
func toFacebox(frameSize:CGSize) -> NormalizedFaceBox {
|
|
||||||
var minX = CGFloat.greatestFiniteMagnitude
|
|
||||||
var minY = CGFloat.greatestFiniteMagnitude
|
|
||||||
var maxX = -CGFloat.greatestFiniteMagnitude
|
|
||||||
var maxY = -CGFloat.greatestFiniteMagnitude
|
|
||||||
points.forEach { point in
|
|
||||||
minX = min(minX, point.x)
|
|
||||||
minY = min(minY, point.y)
|
|
||||||
maxX = max(maxX, point.x)
|
|
||||||
maxY = max(maxY, point.y)
|
|
||||||
}
|
|
||||||
|
|
||||||
return NormalizedFaceBox(
|
|
||||||
xCenter: ((maxX - minX)/2 + minX) / frameSize.width,
|
|
||||||
yCenter: ((maxY - minY)/2 + minY) / frameSize.height,
|
|
||||||
width: (maxX - minX) / frameSize.width,
|
|
||||||
height: (maxY - minY) / frameSize.height
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate extension UIImage {
|
|
||||||
func resizedImage(size: CGSize) -> UIImage? {
|
|
||||||
UIGraphicsBeginImageContextWithOptions(size, false, scale)
|
|
||||||
defer { UIGraphicsEndImageContext() }
|
|
||||||
draw(in: CGRect(origin: .zero, size: size))
|
|
||||||
let image = UIGraphicsGetImageFromCurrentImageContext()
|
|
||||||
return image?.data.flatMap(UIImage.init) // This line is copied from MLKit, Not sure why is needed
|
|
||||||
}
|
|
||||||
|
|
||||||
private var data: Data? {
|
|
||||||
#if swift(>=4.2)
|
|
||||||
return pngData() ?? jpegData(compressionQuality: Constant.jpegCompressionQuality)
|
|
||||||
#else
|
|
||||||
return pngData() ?? jpegData(compressionQuality: Constant.jpegCompressionQuality)
|
|
||||||
#endif // swift(>=4.2)
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum Constant {
|
|
||||||
static let jpegCompressionQuality: CGFloat = 0.8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension VisionPoint : FaceContourPoint {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension FaceContour : Contour {
|
|
||||||
}
|
|
||||||
|
|
||||||
extension VitalSignEngineCore.FaceLandmarkType {
|
|
||||||
var asContourType:MLKitFaceDetection.FaceContourType? {
|
|
||||||
switch self {
|
|
||||||
case .faceOval: return .face
|
|
||||||
case .leftEyebrowTop: return .leftEyebrowTop
|
|
||||||
case .leftEyebrowBottom: return .leftEyebrowBottom
|
|
||||||
case .rightEyebrowTop: return .rightEyebrowTop
|
|
||||||
case .rightEyebrowBottom: return .rightEyebrowBottom
|
|
||||||
case .leftEye: return .leftEye
|
|
||||||
case .rightEye: return .rightEye
|
|
||||||
case .upperLipTop: return .upperLipTop
|
|
||||||
case .upperLipBottom: return .upperLipBottom
|
|
||||||
case .lowerLipTop: return .lowerLipTop
|
|
||||||
case .lowerLipBottom: return .lowerLipBottom
|
|
||||||
case .noseBridge: return .noseBridge
|
|
||||||
case .noseBottom: return .noseBottom
|
|
||||||
case .leftCheek: return .leftCheek
|
|
||||||
case .rightCheek: return .rightCheek
|
|
||||||
@unknown default: return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
import Flutter
|
|
||||||
import UIKit
|
|
||||||
import VitalSignEngineCore
|
|
||||||
|
|
||||||
public class SwiftVitalSignCameraPlugin: NSObject, FlutterPlugin {
|
|
||||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
|
||||||
|
|
||||||
let channel = FlutterMethodChannel(name: "vital_sign_camera_plugin", binaryMessenger: registrar.messenger())
|
|
||||||
let instance = SwiftVitalSignCameraPlugin()
|
|
||||||
registrar.addMethodCallDelegate(instance, channel: channel)
|
|
||||||
|
|
||||||
registrar.register(VitalSignCameraFactory(messenger: registrar.messenger()), withId: "ai.panoptic/flutter_vital_sign_camera")
|
|
||||||
}
|
|
||||||
|
|
||||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
||||||
if call.method == "availableCameraDevices" {
|
|
||||||
let devices = VitalSignCamera.availableCameraDevices()
|
|
||||||
result(devices.map({ $0.flutterCompatibleDict }))
|
|
||||||
}
|
|
||||||
if call.method == "requestCameraPermission" {
|
|
||||||
VitalSignCamera.requestCameraPermission().done { status in
|
|
||||||
result(status.rawValue)
|
|
||||||
}.catch { error in
|
|
||||||
result(FlutterError(code: "\(error.code)", message: error.message, details: nil))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if call.method == "getCameraPermissionStatus" {
|
|
||||||
let status = VitalSignCamera.getCameraPermissionStatus()
|
|
||||||
result(status.rawValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate extension CameraDevice {
|
|
||||||
var flutterCompatibleDict:[String:Any] {
|
|
||||||
[
|
|
||||||
"id": id,
|
|
||||||
"devices": devices.map { $0.rawValue },
|
|
||||||
"position": position.rawValue,
|
|
||||||
"name": name,
|
|
||||||
"hasFlash": NSNumber(value: hasFlash),
|
|
||||||
"hasTorch": NSNumber(value: hasTorch),
|
|
||||||
"minZoom": NSNumber(value: Double(minZoom)),
|
|
||||||
"neutralZoom": NSNumber(value: Double(neutralZoom)),
|
|
||||||
"maxZoom": NSNumber(value: Double(maxZoom)),
|
|
||||||
"isMultiCam": NSNumber(value: isMultiCam),
|
|
||||||
"supportsParallelVideoProcessing": NSNumber(value: supportsParallelVideoProcessing),
|
|
||||||
"supportsDepthCapture": NSNumber(value: supportsDepthCapture),
|
|
||||||
"supportsRawCapture": NSNumber(value: supportsRawCapture),
|
|
||||||
"supportsLowLightBoost": NSNumber(value: supportsLowLightBoost),
|
|
||||||
"supportsFocus": NSNumber(value: supportsFocus),
|
|
||||||
"formats": formats.map { format -> [String: Any] in
|
|
||||||
format.flutterCompatibleDict
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate extension CameraCaptureFormat {
|
|
||||||
var flutterCompatibleDict:[String:Any] {
|
|
||||||
var dict:[String:Any] = [
|
|
||||||
"videoStabilizationModes": videoStabilizationModes.map { $0.rawValue },
|
|
||||||
"autoFocusSystem": autoFocusSystem.rawValue,
|
|
||||||
"photoHeight": NSNumber(value: Double(photoHeight)),
|
|
||||||
"photoWidth": NSNumber(value: Double(photoWidth)),
|
|
||||||
"videoHeight": NSNumber(value: Double(videoHeight)),
|
|
||||||
"videoWidth": NSNumber(value: Double(videoWidth)),
|
|
||||||
"maxISO": NSNumber(value: Int(maxISO)),
|
|
||||||
"minISO": NSNumber(value: Int(minISO)),
|
|
||||||
"fieldOfView": NSNumber(value: Double(fieldOfView)),
|
|
||||||
"maxZoom": NSNumber(value: Double(maxZoom)),
|
|
||||||
"colorSpaces": colorSpaces.map { $0.rawValue },
|
|
||||||
"supportsVideoHDR": NSNumber(value: supportsVideoHDR),
|
|
||||||
"supportsPhotoHDR": NSNumber(value: supportsPhotoHDR),
|
|
||||||
"frameRateRanges": frameRateRanges.map {
|
|
||||||
[
|
|
||||||
"minFrameRate": NSNumber(value: Int($0.minFrameRate)),
|
|
||||||
"maxFrameRate": NSNumber(value: Int($0.maxFrameRate)),
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"pixelFormat": pixelFormat.rawValue,
|
|
||||||
]
|
|
||||||
|
|
||||||
if let isHighestPhotoQualitySupported = isHighestPhotoQualitySupported {
|
|
||||||
dict["isHighestPhotoQualitySupported"] = isHighestPhotoQualitySupported
|
|
||||||
}
|
|
||||||
|
|
||||||
return dict
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
//
|
|
||||||
// VitalSignCameraFactory.swift
|
|
||||||
// vital_sign_camera
|
|
||||||
//
|
|
||||||
// Created by Eddie Lau on 24/1/2023.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Flutter
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class VitalSignCameraFactory: NSObject, FlutterPlatformViewFactory {
|
|
||||||
private var messenger: FlutterBinaryMessenger
|
|
||||||
|
|
||||||
init(messenger: FlutterBinaryMessenger) {
|
|
||||||
self.messenger = messenger
|
|
||||||
super.init()
|
|
||||||
}
|
|
||||||
|
|
||||||
func create(
|
|
||||||
withFrame frame: CGRect,
|
|
||||||
viewIdentifier viewId: Int64,
|
|
||||||
arguments args: Any?
|
|
||||||
) -> FlutterPlatformView {
|
|
||||||
return FlutterVitalSignCamera(
|
|
||||||
frame: frame,
|
|
||||||
viewIdentifier: viewId,
|
|
||||||
arguments: args,
|
|
||||||
binaryMessenger: messenger)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
|
|
||||||
FlutterStandardMessageCodec.sharedInstance()
|
|
||||||
}
|
|
||||||
}
|
|
||||||