Add virtual Face HAL
Fixes: 230514750
Test: atest VtsHalBiometricsFaceTargetTest
Test: atest VtsHalBiometricsFingerprintTargetTest
Test: atest android.hardware.biometrics.face.FakeFaceEngineTest
Test: atest android.hardware.biometrics.face.FakeFingerprintEngineTest
Test: See README.md
Test: Verified that face and fingerprint get reset upon authenticating.
Change-Id: I57c1a61bec960e3be28736e6050be662ef412d8c
diff --git a/biometrics/common/util/include/util/Util.h b/biometrics/common/util/include/util/Util.h
new file mode 100644
index 0000000..29ec0f8
--- /dev/null
+++ b/biometrics/common/util/include/util/Util.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include <android-base/logging.h>
+
+#include <chrono>
+#include <regex>
+#include <thread>
+#include <vector>
+
+namespace aidl::android::hardware::biometrics {
+
+#define SLEEP_MS(x) \
+ if (x > 0) std::this_thread::sleep_for(std::chrono::milliseconds(x))
+#define BEGIN_OP(x) \
+ do { \
+ LOG(INFO) << __func__; \
+ SLEEP_MS(x); \
+ } while (0)
+#define IS_TRUE(x) ((x == "1") || (x == "true"))
+
+// This is for non-test situations, such as casual cuttlefish users, that don't
+// set an explicit value.
+// Some operations (i.e. enroll, authenticate) will be executed in tight loops
+// by parts of the UI or fail if there is no latency. For example, the
+// Face settings page constantly runs auth and the enrollment UI uses a
+// cancel/restart cycle that requires some latency while the activities change.
+#define DEFAULT_LATENCY 800
+
+class Util {
+ public:
+ static int64_t getSystemNanoTime() {
+ timespec now;
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ return now.tv_sec * 1000000000LL + now.tv_nsec;
+ }
+
+ static bool hasElapsed(int64_t start, int64_t durationMillis) {
+ auto now = getSystemNanoTime();
+ if (now < start) return true;
+ if (durationMillis <= 0) return true;
+ return ((now - start) / 1000000LL) > durationMillis;
+ }
+
+ static std::vector<std::string> split(const std::string& str, const std::string& sep) {
+ std::regex regex(sep);
+ std::vector<std::string> parts(
+ std::sregex_token_iterator(str.begin(), str.end(), regex, -1),
+ std::sregex_token_iterator());
+ return parts;
+ }
+};
+
+} // namespace aidl::android::hardware::biometrics
\ No newline at end of file
diff --git a/biometrics/face/aidl/default/Android.bp b/biometrics/face/aidl/default/Android.bp
index 7f66eca..48c929b 100644
--- a/biometrics/face/aidl/default/Android.bp
+++ b/biometrics/face/aidl/default/Android.bp
@@ -18,10 +18,43 @@
"libbinder_ndk",
"android.hardware.biometrics.face-V2-ndk",
"android.hardware.biometrics.common-V2-ndk",
+ "android.hardware.biometrics.common.thread",
+ "android.hardware.biometrics.common.util",
],
srcs: [
"main.cpp",
"Face.cpp",
+ "FakeFaceEngine.cpp",
"Session.cpp",
],
+ static_libs: ["android.hardware.biometrics.face.VirtualProps"],
+}
+
+sysprop_library {
+ name: "android.hardware.biometrics.face.VirtualProps",
+ srcs: ["face.sysprop"],
+ property_owner: "Vendor",
+ vendor: true,
+}
+
+cc_test {
+ name: "android.hardware.biometrics.face.FakeFaceEngineTest",
+ srcs: [
+ "tests/FakeFaceEngineTest.cpp",
+ "FakeFaceEngine.cpp",
+ ],
+ shared_libs: [
+ "libbase",
+ "libbinder_ndk",
+ ],
+ static_libs: [
+ "android.hardware.biometrics.face.VirtualProps",
+ "android.hardware.biometrics.face-V2-ndk",
+ "android.hardware.biometrics.common-V2-ndk",
+ "android.hardware.keymaster-V3-ndk",
+ "android.hardware.biometrics.common.util",
+ ],
+ vendor: true,
+ test_suites: ["general-tests"],
+ require_root: true,
}
diff --git a/biometrics/face/aidl/default/Face.cpp b/biometrics/face/aidl/default/Face.cpp
index aca3e13..652a7e1 100644
--- a/biometrics/face/aidl/default/Face.cpp
+++ b/biometrics/face/aidl/default/Face.cpp
@@ -17,12 +17,14 @@
#include "Face.h"
#include "Session.h"
+#include "FakeFaceEngine.h"
+
namespace aidl::android::hardware::biometrics::face {
const int kSensorId = 4;
-const common::SensorStrength kSensorStrength = common::SensorStrength::STRONG;
+const common::SensorStrength kSensorStrength = FakeFaceEngine::GetSensorStrength();
const int kMaxEnrollmentsPerUser = 5;
-const FaceSensorType kSensorType = FaceSensorType::RGB;
+const FaceSensorType kSensorType = FakeFaceEngine::GetSensorType();
const bool kHalControlsPreview = true;
const std::string kHwComponentId = "faceSensor";
const std::string kHardwareVersion = "vendor/model/revision";
@@ -69,7 +71,7 @@
ndk::ScopedAStatus Face::createSession(int32_t /*sensorId*/, int32_t /*userId*/,
const std::shared_ptr<ISessionCallback>& cb,
std::shared_ptr<ISession>* return_val) {
- *return_val = SharedRefBase::make<Session>(cb);
+ *return_val = SharedRefBase::make<Session>(std::make_unique<FakeFaceEngine>(), cb);
return ndk::ScopedAStatus::ok();
}
diff --git a/biometrics/face/aidl/default/FakeFaceEngine.cpp b/biometrics/face/aidl/default/FakeFaceEngine.cpp
new file mode 100644
index 0000000..0f088f4
--- /dev/null
+++ b/biometrics/face/aidl/default/FakeFaceEngine.cpp
@@ -0,0 +1,318 @@
+#include "FakeFaceEngine.h"
+
+#include <android-base/logging.h>
+
+#include <face.sysprop.h>
+
+#include "util/CancellationSignal.h"
+#include "util/Util.h"
+
+using namespace ::android::face::virt;
+
+namespace aidl::android::hardware::biometrics::face {
+
+FaceSensorType FakeFaceEngine::GetSensorType() {
+ std::string type = FaceHalProperties::type().value_or("");
+ if (type == "IR") {
+ return FaceSensorType::IR;
+ } else {
+ FaceHalProperties::type("RGB");
+ return FaceSensorType::RGB;
+ }
+}
+
+common::SensorStrength FakeFaceEngine::GetSensorStrength() {
+ std::string strength = FaceHalProperties::strength().value_or("");
+ if (strength == "convenience") {
+ return common::SensorStrength::CONVENIENCE;
+ } else if (strength == "weak") {
+ return common::SensorStrength::WEAK;
+ } else {
+ FaceHalProperties::strength("strong");
+ return common::SensorStrength::STRONG;
+ }
+}
+
+void FakeFaceEngine::generateChallengeImpl(ISessionCallback* cb) {
+ BEGIN_OP(0);
+ std::uniform_int_distribution<int64_t> dist;
+ auto challenge = dist(mRandom);
+ FaceHalProperties::challenge(challenge);
+ cb->onChallengeGenerated(challenge);
+}
+
+void FakeFaceEngine::revokeChallengeImpl(ISessionCallback* cb, int64_t challenge) {
+ BEGIN_OP(0);
+ FaceHalProperties::challenge({});
+ cb->onChallengeRevoked(challenge);
+}
+void FakeFaceEngine::getEnrollmentConfigImpl(ISessionCallback* /*cb*/,
+ std::vector<EnrollmentStageConfig>* /*return_val*/) {}
+void FakeFaceEngine::enrollImpl(ISessionCallback* cb, const keymaster::HardwareAuthToken& hat,
+ EnrollmentType /*enrollmentType*/,
+ const std::vector<Feature>& /*features*/,
+ const std::future<void>& cancel) {
+ BEGIN_OP(FaceHalProperties::operation_start_enroll_latency().value_or(0));
+ // format is "<id>,<bucket_id>:<delay>:<succeeds>,<bucket_id>:<delay>:<succeeds>...
+ auto nextEnroll = FaceHalProperties::next_enrollment().value_or("");
+ // Erase the next enrollment
+ FaceHalProperties::next_enrollment({});
+
+ AuthenticationFrame frame;
+ frame.data.acquiredInfo = AcquiredInfo::START;
+ frame.data.vendorCode = 0;
+ cb->onAuthenticationFrame(frame);
+
+ // Do proper HAT verification in the real implementation.
+ if (hat.mac.empty()) {
+ LOG(ERROR) << "Fail: hat";
+ cb->onError(Error::UNABLE_TO_PROCESS, 0 /* vendorError */);
+ return;
+ }
+
+ if (FaceHalProperties::operation_enroll_fails().value_or(false)) {
+ LOG(ERROR) << "Fail: operation_enroll_fails";
+ cb->onError(Error::VENDOR, 0 /* vendorError */);
+ return;
+ }
+
+ auto parts = Util::split(nextEnroll, ",");
+ if (parts.size() < 2) {
+ LOG(ERROR) << "Fail: invalid next_enrollment for : " << nextEnroll;
+ cb->onError(Error::VENDOR, 0 /* vendorError */);
+ return;
+ }
+
+ auto enrollmentId = std::stoi(parts[0]);
+ const int numBuckets = parts.size() - 1;
+ for (size_t i = 1; i < parts.size(); i++) {
+ auto enrollHit = Util::split(parts[i], ":");
+ if (enrollHit.size() != 3) {
+ LOG(ERROR) << "Error when unpacking enrollment hit: " << parts[i];
+ cb->onError(Error::VENDOR, 0 /* vendorError */);
+ }
+ std::string bucket = enrollHit[0];
+ std::string delay = enrollHit[1];
+ std::string succeeds = enrollHit[2];
+
+ SLEEP_MS(std::stoi(delay));
+
+ if (shouldCancel(cancel)) {
+ LOG(ERROR) << "Fail: cancel";
+ cb->onError(Error::CANCELED, 0 /* vendorCode */);
+ return;
+ }
+
+ if (!IS_TRUE(succeeds)) { // end and failed
+ LOG(ERROR) << "Fail: requested by caller: " << parts[i];
+ cb->onError(Error::UNABLE_TO_PROCESS, 0 /* vendorCode */);
+ return;
+ }
+
+ EnrollmentFrame frame;
+
+ frame.data.acquiredInfo = AcquiredInfo::GOOD;
+ frame.data.vendorCode = 0;
+ cb->onEnrollmentFrame(frame);
+
+ frame.data.acquiredInfo = AcquiredInfo::VENDOR;
+ frame.data.vendorCode = std::stoi(bucket);
+ cb->onEnrollmentFrame(frame);
+
+ int remainingBuckets = numBuckets - i;
+ if (remainingBuckets > 0) {
+ cb->onEnrollmentProgress(enrollmentId, remainingBuckets);
+ }
+ }
+
+ auto enrollments = FaceHalProperties::enrollments();
+ enrollments.push_back(enrollmentId);
+ FaceHalProperties::enrollments(enrollments);
+ LOG(INFO) << "enrolled : " << enrollmentId;
+ cb->onEnrollmentProgress(enrollmentId, 0);
+}
+
+void FakeFaceEngine::authenticateImpl(ISessionCallback* cb, int64_t /*operationId*/,
+ const std::future<void>& cancel) {
+ BEGIN_OP(FaceHalProperties::operation_authenticate_latency().value_or(0));
+
+ // Signal to the framework that we have begun authenticating.
+ AuthenticationFrame frame;
+ frame.data.acquiredInfo = AcquiredInfo::START;
+ frame.data.vendorCode = 0;
+ cb->onAuthenticationFrame(frame);
+
+ // Also signal that we have opened the camera.
+ frame = {};
+ frame.data.acquiredInfo = AcquiredInfo::FIRST_FRAME_RECEIVED;
+ frame.data.vendorCode = 0;
+ cb->onAuthenticationFrame(frame);
+
+ auto now = Util::getSystemNanoTime();
+ int64_t duration = FaceHalProperties::operation_authenticate_duration().value_or(0);
+ if (duration > 0) {
+ do {
+ SLEEP_MS(5);
+ } while (!Util::hasElapsed(now, duration));
+ }
+
+ if (FaceHalProperties::operation_authenticate_fails().value_or(false)) {
+ LOG(ERROR) << "Fail: operation_authenticate_fails";
+ cb->onError(Error::VENDOR, 0 /* vendorError */);
+ return;
+ }
+
+ if (FaceHalProperties::lockout().value_or(false)) {
+ LOG(ERROR) << "Fail: lockout";
+ cb->onLockoutPermanent();
+ cb->onError(Error::HW_UNAVAILABLE, 0 /* vendorError */);
+ return;
+ }
+
+ if (shouldCancel(cancel)) {
+ LOG(ERROR) << "Fail: cancel";
+ cb->onError(Error::CANCELED, 0 /* vendorCode */);
+ return;
+ }
+
+ auto id = FaceHalProperties::enrollment_hit().value_or(0);
+ auto enrolls = FaceHalProperties::enrollments();
+ auto isEnrolled = std::find(enrolls.begin(), enrolls.end(), id) != enrolls.end();
+ if (id < 0 || !isEnrolled) {
+ LOG(ERROR) << (isEnrolled ? "invalid enrollment hit" : "Fail: not enrolled");
+ cb->onAuthenticationFailed();
+ cb->onError(Error::UNABLE_TO_PROCESS, 0 /* vendorError */);
+ return;
+ }
+
+ cb->onAuthenticationSucceeded(id, {} /* hat */);
+}
+
+void FakeFaceEngine::detectInteractionImpl(ISessionCallback* cb, const std::future<void>& cancel) {
+ BEGIN_OP(FaceHalProperties::operation_detect_interaction_latency().value_or(0));
+
+ if (FaceHalProperties::operation_detect_interaction_fails().value_or(false)) {
+ LOG(ERROR) << "Fail: operation_detect_interaction_fails";
+ cb->onError(Error::VENDOR, 0 /* vendorError */);
+ return;
+ }
+
+ if (shouldCancel(cancel)) {
+ LOG(ERROR) << "Fail: cancel";
+ cb->onError(Error::CANCELED, 0 /* vendorCode */);
+ return;
+ }
+
+ auto id = FaceHalProperties::enrollment_hit().value_or(0);
+ auto enrolls = FaceHalProperties::enrollments();
+ auto isEnrolled = std::find(enrolls.begin(), enrolls.end(), id) != enrolls.end();
+ if (id <= 0 || !isEnrolled) {
+ LOG(ERROR) << "Fail: not enrolled";
+ cb->onError(Error::UNABLE_TO_PROCESS, 0 /* vendorError */);
+ return;
+ }
+
+ cb->onInteractionDetected();
+}
+
+void FakeFaceEngine::enumerateEnrollmentsImpl(ISessionCallback* cb) {
+ BEGIN_OP(0);
+ std::vector<int32_t> enrollments;
+ for (const auto& enrollmentId : FaceHalProperties::enrollments()) {
+ if (enrollmentId) {
+ enrollments.push_back(*enrollmentId);
+ }
+ }
+ cb->onEnrollmentsEnumerated(enrollments);
+}
+
+void FakeFaceEngine::removeEnrollmentsImpl(ISessionCallback* cb,
+ const std::vector<int32_t>& enrollmentIds) {
+ BEGIN_OP(0);
+
+ std::vector<std::optional<int32_t>> newEnrollments;
+ for (const auto& enrollment : FaceHalProperties::enrollments()) {
+ auto id = enrollment.value_or(0);
+ if (std::find(enrollmentIds.begin(), enrollmentIds.end(), id) == enrollmentIds.end()) {
+ newEnrollments.emplace_back(id);
+ }
+ }
+ FaceHalProperties::enrollments(newEnrollments);
+ cb->onEnrollmentsRemoved(enrollmentIds);
+}
+
+void FakeFaceEngine::getFeaturesImpl(ISessionCallback* cb) {
+ BEGIN_OP(0);
+
+ if (FaceHalProperties::enrollments().empty()) {
+ cb->onError(Error::UNABLE_TO_PROCESS, 0 /* vendorCode */);
+ return;
+ }
+
+ std::vector<Feature> featuresToReturn = {};
+ for (const auto& feature : FaceHalProperties::features()) {
+ if (feature) {
+ featuresToReturn.push_back((Feature)(*feature));
+ }
+ }
+ cb->onFeaturesRetrieved(featuresToReturn);
+}
+
+void FakeFaceEngine::setFeatureImpl(ISessionCallback* cb, const keymaster::HardwareAuthToken& hat,
+ Feature feature, bool enabled) {
+ BEGIN_OP(0);
+
+ if (FaceHalProperties::enrollments().empty()) {
+ LOG(ERROR) << "Unable to set feature, enrollments are empty";
+ cb->onError(Error::UNABLE_TO_PROCESS, 0 /* vendorCode */);
+ return;
+ }
+
+ if (hat.mac.empty()) {
+ LOG(ERROR) << "Unable to set feature, invalid hat";
+ cb->onError(Error::UNABLE_TO_PROCESS, 0 /* vendorCode */);
+ return;
+ }
+
+ auto features = FaceHalProperties::features();
+
+ auto itr = std::find_if(features.begin(), features.end(), [feature](const auto& theFeature) {
+ return *theFeature == (int)feature;
+ });
+
+ if (!enabled && (itr != features.end())) {
+ features.erase(itr);
+ } else if (enabled && (itr == features.end())) {
+ features.push_back((int)feature);
+ }
+
+ FaceHalProperties::features(features);
+ cb->onFeatureSet(feature);
+}
+
+void FakeFaceEngine::getAuthenticatorIdImpl(ISessionCallback* cb) {
+ BEGIN_OP(0);
+ // If this is a weak HAL return 0 per the spec.
+ if (GetSensorStrength() != common::SensorStrength::STRONG) {
+ cb->onAuthenticatorIdRetrieved(0);
+ } else {
+ cb->onAuthenticatorIdRetrieved(FaceHalProperties::authenticator_id().value_or(0));
+ }
+}
+
+void FakeFaceEngine::invalidateAuthenticatorIdImpl(ISessionCallback* cb) {
+ BEGIN_OP(0);
+ int64_t authenticatorId = FaceHalProperties::authenticator_id().value_or(0);
+ int64_t newId = authenticatorId + 1;
+ FaceHalProperties::authenticator_id(newId);
+ cb->onAuthenticatorIdInvalidated(newId);
+}
+
+void FakeFaceEngine::resetLockoutImpl(ISessionCallback* cb,
+ const keymaster::HardwareAuthToken& /*hat*/) {
+ BEGIN_OP(0);
+ FaceHalProperties::lockout(false);
+ cb->onLockoutCleared();
+}
+
+} // namespace aidl::android::hardware::biometrics::face
\ No newline at end of file
diff --git a/biometrics/face/aidl/default/FakeFaceEngine.h b/biometrics/face/aidl/default/FakeFaceEngine.h
new file mode 100644
index 0000000..edb54ce
--- /dev/null
+++ b/biometrics/face/aidl/default/FakeFaceEngine.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include <aidl/android/hardware/biometrics/common/SensorStrength.h>
+#include <aidl/android/hardware/biometrics/face/BnSession.h>
+#include <aidl/android/hardware/biometrics/face/FaceSensorType.h>
+#include <aidl/android/hardware/biometrics/face/ISessionCallback.h>
+
+#include <random>
+
+#include <future>
+#include <vector>
+
+namespace aidl::android::hardware::biometrics::face {
+
+namespace face = aidl::android::hardware::biometrics::face;
+namespace common = aidl::android::hardware::biometrics::common;
+namespace keymaster = aidl::android::hardware::keymaster;
+
+using aidl::android::hardware::common::NativeHandle;
+// A fake engine that is backed by system properties instead of hardware.
+class FakeFaceEngine {
+ public:
+ FakeFaceEngine() : mRandom(std::mt19937::default_seed) {}
+
+ static face::FaceSensorType GetSensorType();
+ static common::SensorStrength GetSensorStrength();
+ void generateChallengeImpl(ISessionCallback* cb);
+ void revokeChallengeImpl(ISessionCallback* cb, int64_t challenge);
+ void getEnrollmentConfigImpl(ISessionCallback* cb,
+ std::vector<EnrollmentStageConfig>* return_val);
+ void enrollImpl(ISessionCallback* cb, const keymaster::HardwareAuthToken& hat,
+ EnrollmentType enrollmentType, const std::vector<Feature>& features,
+ const std::future<void>& cancel);
+ void authenticateImpl(ISessionCallback* cb, int64_t operationId,
+ const std::future<void>& cancel);
+ void detectInteractionImpl(ISessionCallback* cb, const std::future<void>& cancel);
+ void enumerateEnrollmentsImpl(ISessionCallback* cb);
+ void removeEnrollmentsImpl(ISessionCallback* cb, const std::vector<int32_t>& enrollmentIds);
+ void getFeaturesImpl(ISessionCallback* cb);
+ void setFeatureImpl(ISessionCallback* cb, const keymaster::HardwareAuthToken& hat,
+ Feature feature, bool enabled);
+ void getAuthenticatorIdImpl(ISessionCallback* cb);
+ void invalidateAuthenticatorIdImpl(ISessionCallback* cb);
+ void resetLockoutImpl(ISessionCallback* cb, const keymaster::HardwareAuthToken& /*hat*/);
+
+ std::mt19937 mRandom;
+};
+
+} // namespace aidl::android::hardware::biometrics::face
\ No newline at end of file
diff --git a/biometrics/face/aidl/default/README.md b/biometrics/face/aidl/default/README.md
new file mode 100644
index 0000000..1655973
--- /dev/null
+++ b/biometrics/face/aidl/default/README.md
@@ -0,0 +1,77 @@
+# Virtual Face HAL
+
+This is a virtual HAL implementation that is backed by system properties
+instead of actual hardware. It's intended for testing and UI development
+on debuggable builds to allow devices to masquerade as alternative device
+types and for emulators.
+
+## Device Selection
+
+You can either run the FakeFaceEngine on a [real device](#actual-device) or a [virtual device/cuttlefish](#getting-started-on-a-virtual-device-cuttlefish). This document should
+help you to get started on either one.
+
+After setting up a device, go ahead and try out [enrolling](#enrolling) & [authenticating](#authenticating)
+
+### Getting started on a Virtual Device (cuttlefish)
+
+
+Note, I'm running this via a cloudtop virtual device.
+
+1. Setup cuttlefish on cloudtop, See [this](https://g3doc.corp.google.com/company/teams/android/teampages/acloud/getting_started.md?cl=head) for more details.
+2. acloud create --local-image
+3. Enter in the shell command to disable hidl
+
+```shell
+$ adb root
+$ adb shell settings put secure com.android.server.biometrics.AuthService.hidlDisabled 1
+$ adb reboot
+```
+4. You should now be able to do fake enrollments and authentications (as seen down below)
+
+### Actual Device
+
+1. Modify your real devices make file (I.E. vendor/google/products/{YOUR_DEVICE}.mk)
+2. Ensure that there is no other face HAL that is being included by the device
+3. Add the following
+```
+PRODUCT_COPY_FILES += \
+ frameworks/native/data/etc/android.hardware.biometrics.face.xml:$(TARGET_COPY_OUT_PRODUCT)/etc/permissions/android.hardware.biometrics.face.xml
+
+PRODUCT_PACKAGES += \
+ android.hardware.biometrics.face-service.example \
+
+```
+4. Now build and flash m -j120 && flash
+5. Run the following commands
+
+```shell
+# This is a temporary workaround
+$ adb root
+$ adb shell setprop persist.vendor.face.virtual.type RGB
+$ adb shell setprop persist.vendor.face.virtual.strength strong
+$ adb shell locksettings set-pin 0000
+$ adb reboot
+```
+
+## Enrolling
+
+```shell
+# authenticar_id,bucket_id:duration:(true|false)....
+$ adb shell setprop vendor.face.virtual.next_enrollment 1,0:500:true,5:250:true,10:150:true,15:500:true
+$ adb shell am start -n com.android.settings/.biometrics.face.FaceEnrollIntroduction
+# If you would like to get rid of the enrollment, run the follwoing command
+$ adb shell setprop persist.vendor.face.virtual.enrollments \"\"
+```
+
+## Authenticating
+
+```shell
+# If enrollment hasn't been setup
+$ adb shell setprop persist.vendor.face.virtual.enrollments 1
+$ adb shell cmd face sync
+# After enrollment has been setup
+$ adb shell setprop vendor.face.virtual.operation_authenticate_duration 800
+$ adb shell setprop vendor.face.virtual.enrollment_hit 1
+# Power button press to simulate auth
+$ adb shell input keyevent 26
+```
diff --git a/biometrics/face/aidl/default/Session.cpp b/biometrics/face/aidl/default/Session.cpp
index 984a1a9..1188459 100644
--- a/biometrics/face/aidl/default/Session.cpp
+++ b/biometrics/face/aidl/default/Session.cpp
@@ -14,139 +14,135 @@
* limitations under the License.
*/
-#include <aidl/android/hardware/biometrics/common/BnCancellationSignal.h>
#include <android-base/logging.h>
#include "Session.h"
namespace aidl::android::hardware::biometrics::face {
-class CancellationSignal : public common::BnCancellationSignal {
- private:
- std::shared_ptr<ISessionCallback> cb_;
+constexpr size_t MAX_WORKER_QUEUE_SIZE = 5;
- public:
- explicit CancellationSignal(std::shared_ptr<ISessionCallback> cb) : cb_(std::move(cb)) {}
-
- ndk::ScopedAStatus cancel() override {
- cb_->onError(Error::CANCELED, 0 /* vendorCode */);
- return ndk::ScopedAStatus::ok();
- }
-};
-
-Session::Session(std::shared_ptr<ISessionCallback> cb)
- : cb_(std::move(cb)), mRandom(std::mt19937::default_seed) {}
+Session::Session(std::unique_ptr<FakeFaceEngine> engine, std::shared_ptr<ISessionCallback> cb)
+ : mEngine(std::move(engine)), mCb(std::move(cb)), mRandom(std::mt19937::default_seed) {
+ mThread = std::make_unique<WorkerThread>(MAX_WORKER_QUEUE_SIZE);
+}
ndk::ScopedAStatus Session::generateChallenge() {
LOG(INFO) << "generateChallenge";
- if (cb_) {
- std::uniform_int_distribution<int64_t> dist;
- auto challenge = dist(mRandom);
- cb_->onChallengeGenerated(challenge);
- }
+ mThread->schedule(Callable::from([this] { mEngine->generateChallengeImpl(mCb.get()); }));
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus Session::revokeChallenge(int64_t challenge) {
LOG(INFO) << "revokeChallenge";
- if (cb_) {
- cb_->onChallengeRevoked(challenge);
- }
+ mThread->schedule(Callable::from(
+ [this, challenge] { mEngine->revokeChallengeImpl(mCb.get(), challenge); }));
return ndk::ScopedAStatus::ok();
}
-ndk::ScopedAStatus Session::getEnrollmentConfig(EnrollmentType /*enrollmentType*/,
- std::vector<EnrollmentStageConfig>* return_val) {
- *return_val = {};
+ndk::ScopedAStatus Session::getEnrollmentConfig(
+ EnrollmentType /*enrollmentType*/, std::vector<EnrollmentStageConfig>* cancellationSignal) {
+ *cancellationSignal = {};
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus Session::enroll(
- const keymaster::HardwareAuthToken& /*hat*/, EnrollmentType /*enrollmentType*/,
- const std::vector<Feature>& /*features*/,
- const std::optional<NativeHandle>& /*previewSurface*/,
- std::shared_ptr<biometrics::common::ICancellationSignal>* /*return_val*/) {
+ const keymaster::HardwareAuthToken& hat, EnrollmentType enrollmentType,
+ const std::vector<Feature>& features, const std::optional<NativeHandle>& /*previewSurface*/,
+ std::shared_ptr<biometrics::common::ICancellationSignal>* cancellationSignal) {
LOG(INFO) << "enroll";
- if (cb_) {
- cb_->onError(Error::UNABLE_TO_PROCESS, 0 /* vendorError */);
- }
+ std::promise<void> cancellationPromise;
+ auto cancFuture = cancellationPromise.get_future();
+
+ mThread->schedule(Callable::from(
+ [this, hat, enrollmentType, features, cancFuture = std::move(cancFuture)] {
+ mEngine->enrollImpl(mCb.get(), hat, enrollmentType, features, cancFuture);
+ }));
+
+ *cancellationSignal = SharedRefBase::make<CancellationSignal>(std::move(cancellationPromise));
return ndk::ScopedAStatus::ok();
}
-ndk::ScopedAStatus Session::authenticate(int64_t /*keystoreOperationId*/,
- std::shared_ptr<common::ICancellationSignal>* return_val) {
+ndk::ScopedAStatus Session::authenticate(
+ int64_t keystoreOperationId,
+ std::shared_ptr<common::ICancellationSignal>* cancellationSignal) {
LOG(INFO) << "authenticate";
- if (cb_) {
- cb_->onError(Error::UNABLE_TO_PROCESS, 0 /* vendorCode */);
- }
- *return_val = SharedRefBase::make<CancellationSignal>(cb_);
+ std::promise<void> cancellationPromise;
+ auto cancFuture = cancellationPromise.get_future();
+
+ mThread->schedule(
+ Callable::from([this, keystoreOperationId, cancFuture = std::move(cancFuture)] {
+ mEngine->authenticateImpl(mCb.get(), keystoreOperationId, cancFuture);
+ }));
+
+ *cancellationSignal = SharedRefBase::make<CancellationSignal>(std::move(cancellationPromise));
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus Session::detectInteraction(
- std::shared_ptr<common::ICancellationSignal>* /*return_val*/) {
+ std::shared_ptr<common::ICancellationSignal>* cancellationSignal) {
LOG(INFO) << "detectInteraction";
+ std::promise<void> cancellationPromise;
+ auto cancFuture = cancellationPromise.get_future();
+
+ mThread->schedule(Callable::from([this, cancFuture = std::move(cancFuture)] {
+ mEngine->detectInteractionImpl(mCb.get(), cancFuture);
+ }));
+
+ *cancellationSignal = SharedRefBase::make<CancellationSignal>(std::move(cancellationPromise));
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus Session::enumerateEnrollments() {
LOG(INFO) << "enumerateEnrollments";
- if (cb_) {
- cb_->onEnrollmentsEnumerated(std::vector<int32_t>());
- }
+ mThread->schedule(Callable::from([this] { mEngine->enumerateEnrollmentsImpl(mCb.get()); }));
return ndk::ScopedAStatus::ok();
}
-ndk::ScopedAStatus Session::removeEnrollments(const std::vector<int32_t>& /*enrollmentIds*/) {
+ndk::ScopedAStatus Session::removeEnrollments(const std::vector<int32_t>& enrollmentIds) {
LOG(INFO) << "removeEnrollments";
- if (cb_) {
- cb_->onEnrollmentsRemoved(std::vector<int32_t>());
- }
+ mThread->schedule(Callable::from(
+ [this, enrollmentIds] { mEngine->removeEnrollmentsImpl(mCb.get(), enrollmentIds); }));
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus Session::getFeatures() {
LOG(INFO) << "getFeatures";
- if (cb_) {
- // Must error out with UNABLE_TO_PROCESS when no faces are enrolled.
- cb_->onError(Error::UNABLE_TO_PROCESS, 0 /* vendorCode */);
- }
+ mThread->schedule(Callable::from([this] { mEngine->getFeaturesImpl(mCb.get()); }));
return ndk::ScopedAStatus::ok();
}
-ndk::ScopedAStatus Session::setFeature(const keymaster::HardwareAuthToken& /*hat*/,
- Feature /*feature*/, bool /*enabled*/) {
+ndk::ScopedAStatus Session::setFeature(const keymaster::HardwareAuthToken& hat, Feature feature,
+ bool enabled) {
LOG(INFO) << "setFeature";
+ mThread->schedule(Callable::from([this, hat, feature, enabled] {
+ mEngine->setFeatureImpl(mCb.get(), hat, feature, enabled);
+ }));
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus Session::getAuthenticatorId() {
LOG(INFO) << "getAuthenticatorId";
- if (cb_) {
- cb_->onAuthenticatorIdRetrieved(0 /* authenticatorId */);
- }
+ mThread->schedule(Callable::from([this] { mEngine->getAuthenticatorIdImpl(mCb.get()); }));
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus Session::invalidateAuthenticatorId() {
LOG(INFO) << "invalidateAuthenticatorId";
- if (cb_) {
- cb_->onAuthenticatorIdInvalidated(0);
- }
+ mThread->schedule(
+ Callable::from([this] { mEngine->invalidateAuthenticatorIdImpl(mCb.get()); }));
return ndk::ScopedAStatus::ok();
}
-ndk::ScopedAStatus Session::resetLockout(const keymaster::HardwareAuthToken& /*hat*/) {
+ndk::ScopedAStatus Session::resetLockout(const keymaster::HardwareAuthToken& hat) {
LOG(INFO) << "resetLockout";
- if (cb_) {
- cb_->onLockoutCleared();
- }
+ mThread->schedule(Callable::from([this, hat] { mEngine->resetLockoutImpl(mCb.get(), hat); }));
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus Session::close() {
- if (cb_) {
- cb_->onSessionClosed();
+ if (mCb) {
+ mCb->onSessionClosed();
}
return ndk::ScopedAStatus::ok();
}
diff --git a/biometrics/face/aidl/default/Session.h b/biometrics/face/aidl/default/Session.h
index 9db17d2..7ca6a1f 100644
--- a/biometrics/face/aidl/default/Session.h
+++ b/biometrics/face/aidl/default/Session.h
@@ -21,6 +21,10 @@
#include <aidl/android/hardware/biometrics/face/BnSession.h>
#include <aidl/android/hardware/biometrics/face/ISessionCallback.h>
+#include "FakeFaceEngine.h"
+#include "thread/WorkerThread.h"
+#include "util/CancellationSignal.h"
+
namespace aidl::android::hardware::biometrics::face {
namespace common = aidl::android::hardware::biometrics::common;
@@ -30,7 +34,7 @@
class Session : public BnSession {
public:
- explicit Session(std::shared_ptr<ISessionCallback> cb);
+ explicit Session(std::unique_ptr<FakeFaceEngine> engine, std::shared_ptr<ISessionCallback> cb);
ndk::ScopedAStatus generateChallenge() override;
@@ -85,8 +89,11 @@
ndk::ScopedAStatus onContextChanged(const common::OperationContext& context) override;
private:
- std::shared_ptr<ISessionCallback> cb_;
+ std::unique_ptr<FakeFaceEngine> mEngine;
+ std::shared_ptr<ISessionCallback> mCb;
std::mt19937 mRandom;
+ std::unique_ptr<WorkerThread> mThread;
+ std::shared_ptr<CancellationSignal> mCancellationSignal;
};
} // namespace aidl::android::hardware::biometrics::face
diff --git a/biometrics/face/aidl/default/api/android.hardware.biometrics.face.VirtualProps-current.txt b/biometrics/face/aidl/default/api/android.hardware.biometrics.face.VirtualProps-current.txt
new file mode 100644
index 0000000..9548920
--- /dev/null
+++ b/biometrics/face/aidl/default/api/android.hardware.biometrics.face.VirtualProps-current.txt
@@ -0,0 +1,98 @@
+props {
+ owner: Vendor
+ module: "android.face.virt.FaceHalProperties"
+ prop {
+ api_name: "authenticator_id"
+ type: Long
+ access: ReadWrite
+ prop_name: "vendor.face.virtual.authenticator_id"
+ }
+ prop {
+ api_name: "challenge"
+ type: Long
+ access: ReadWrite
+ prop_name: "vendor.face.virtual.challenge"
+ }
+ prop {
+ api_name: "enrollment_hit"
+ type: Integer
+ access: ReadWrite
+ prop_name: "vendor.face.virtual.enrollment_hit"
+ }
+ prop {
+ api_name: "enrollments"
+ type: IntegerList
+ access: ReadWrite
+ prop_name: "persist.vendor.face.virtual.enrollments"
+ }
+ prop {
+ api_name: "features"
+ type: IntegerList
+ access: ReadWrite
+ prop_name: "persist.vendor.face.virtual.features"
+ }
+ prop {
+ api_name: "lockout"
+ access: ReadWrite
+ prop_name: "vendor.face.virtual.lockout"
+ }
+ prop {
+ api_name: "next_enrollment"
+ type: String
+ access: ReadWrite
+ prop_name: "vendor.face.virtual.next_enrollment"
+ }
+ prop {
+ api_name: "operation_authenticate_duration"
+ type: Integer
+ access: ReadWrite
+ prop_name: "vendor.face.virtual.operation_authenticate_duration"
+ }
+ prop {
+ api_name: "operation_authenticate_fails"
+ access: ReadWrite
+ prop_name: "vendor.face.virtual.operation_authenticate_fails"
+ }
+ prop {
+ api_name: "operation_authenticate_latency"
+ type: Integer
+ access: ReadWrite
+ prop_name: "vendor.face.virtual.operation_authenticate_latency"
+ }
+ prop {
+ api_name: "operation_detect_interaction_fails"
+ access: ReadWrite
+ prop_name: "vendor.face.virtual.operation_detect_interaction_fails"
+ }
+ prop {
+ api_name: "operation_detect_interaction_latency"
+ type: Integer
+ access: ReadWrite
+ prop_name: "vendor.face.virtual.operation_detect_interaction_latency"
+ }
+ prop {
+ api_name: "operation_enroll_fails"
+ access: ReadWrite
+ prop_name: "vendor.face.virtual.operation_enroll_fails"
+ }
+ prop {
+ api_name: "operation_start_enroll_latency"
+ type: Integer
+ access: ReadWrite
+ prop_name: "vendor.face.virtual.operation_start_enroll_latency"
+ }
+ prop {
+ api_name: "strength"
+ type: String
+ access: ReadWrite
+ prop_name: "persist.vendor.face.virtual.strength"
+ enum_values: "convenience|weak|strong"
+ }
+ prop {
+ api_name: "type"
+ type: String
+ access: ReadWrite
+ prop_name: "persist.vendor.face.virtual.type"
+ enum_values: "IR|RGB"
+ }
+}
diff --git a/biometrics/face/aidl/default/face.sysprop b/biometrics/face/aidl/default/face.sysprop
new file mode 100644
index 0000000..6b0f37f
--- /dev/null
+++ b/biometrics/face/aidl/default/face.sysprop
@@ -0,0 +1,159 @@
+# face.sysprop
+# module becomes static class (Java) / namespace (C++) for serving API
+module: "android.face.virt.FaceHalProperties"
+owner: Vendor
+
+# type of face sensor
+prop {
+ prop_name: "persist.vendor.face.virtual.type"
+ type: String
+ scope: Public
+ access: ReadWrite
+ enum_values: "IR|RGB"
+ api_name: "type"
+}
+
+# the strength of the sensor
+prop {
+ prop_name: "persist.vendor.face.virtual.strength"
+ type: String
+ scope: Public
+ access: ReadWrite
+ enum_values: "convenience|weak|strong"
+ api_name: "strength"
+}
+
+# ids of current enrollments
+prop {
+ prop_name: "persist.vendor.face.virtual.enrollments"
+ type: IntegerList
+ scope: Public
+ access: ReadWrite
+ api_name: "enrollments"
+}
+
+# List of features
+prop {
+ prop_name: "persist.vendor.face.virtual.features"
+ type: IntegerList
+ scope: Public
+ access: ReadWrite
+ api_name: "features"
+}
+
+# authenticate and detectInteraction will succeed with this
+# enrollment id, when present, otherwise they will error
+prop {
+ prop_name: "vendor.face.virtual.enrollment_hit"
+ type: Integer
+ scope: Public
+ access: ReadWrite
+ api_name: "enrollment_hit"
+}
+
+# The initial latency for enrollment
+prop {
+ prop_name: "vendor.face.virtual.operation_start_enroll_latency"
+ type: Integer
+ scope: Public
+ access: ReadWrite
+ api_name: "operation_start_enroll_latency"
+}
+
+# the next enrollment in the format:
+# "<id>,<bucket_id>:<delay>:<succeeds>,<bucket_id>..."
+# for example: "0:1,0:100:1,1:200:1" indicating that bucket 0 took
+# 50 milliseconds, bucket 1 took 100 milliseconds, bucket 2 took 200 milliseconds.
+# Note that it is up to the configuration to determine how many buckets are required
+# to complete an enrollment
+prop {
+ prop_name: "vendor.face.virtual.next_enrollment"
+ type: String
+ scope: Public
+ access: ReadWrite
+ api_name: "next_enrollment"
+}
+
+# value for getAuthenticatorId or 0
+prop {
+ prop_name: "vendor.face.virtual.authenticator_id"
+ type: Long
+ scope: Public
+ access: ReadWrite
+ api_name: "authenticator_id"
+}
+
+# value for generateChallenge
+prop {
+ prop_name: "vendor.face.virtual.challenge"
+ type: Long
+ scope: Public
+ access: ReadWrite
+ api_name: "challenge"
+}
+
+# if locked out
+prop {
+ prop_name: "vendor.face.virtual.lockout"
+ type: Boolean
+ scope: Public
+ access: ReadWrite
+ api_name: "lockout"
+}
+
+# force all authenticate operations to fail
+prop {
+ prop_name: "vendor.face.virtual.operation_authenticate_fails"
+ type: Boolean
+ scope: Public
+ access: ReadWrite
+ api_name: "operation_authenticate_fails"
+}
+
+# force all detectInteraction operations to fail
+prop {
+ prop_name: "vendor.face.virtual.operation_detect_interaction_fails"
+ type: Boolean
+ scope: Public
+ access: ReadWrite
+ api_name: "operation_detect_interaction_fails"
+}
+
+# force all enroll operations to fail
+prop {
+ prop_name: "vendor.face.virtual.operation_enroll_fails"
+ type: Boolean
+ scope: Public
+ access: ReadWrite
+ api_name: "operation_enroll_fails"
+}
+
+# add a latency to authentication operations
+# Note that this latency is the initial authentication latency that occurs before
+# the HAL will send AcquiredInfo::START and AcquiredInfo::FIRST_FRAME_RECEIVED
+prop {
+ prop_name: "vendor.face.virtual.operation_authenticate_latency"
+ type: Integer
+ scope: Public
+ access: ReadWrite
+ api_name: "operation_authenticate_latency"
+}
+
+# add a latency to detectInteraction operations
+prop {
+ prop_name: "vendor.face.virtual.operation_detect_interaction_latency"
+ type: Integer
+ scope: Public
+ access: ReadWrite
+ api_name: "operation_detect_interaction_latency"
+}
+
+# millisecond duration for authenticate operations
+# (waits for changes to enrollment_hit)
+prop {
+ prop_name: "vendor.face.virtual.operation_authenticate_duration"
+ type: Integer
+ scope: Public
+ access: ReadWrite
+ api_name: "operation_authenticate_duration"
+}
diff --git a/biometrics/face/aidl/default/tests/FakeFaceEngineTest.cpp b/biometrics/face/aidl/default/tests/FakeFaceEngineTest.cpp
new file mode 100644
index 0000000..c8ad6b7
--- /dev/null
+++ b/biometrics/face/aidl/default/tests/FakeFaceEngineTest.cpp
@@ -0,0 +1,383 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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.
+ */
+
+#include <android/binder_process.h>
+#include <face.sysprop.h>
+#include <gtest/gtest.h>
+
+#include <aidl/android/hardware/biometrics/face/BnSessionCallback.h>
+#include <android-base/logging.h>
+
+#include "FakeFaceEngine.h"
+
+using namespace ::android::face::virt;
+using namespace ::aidl::android::hardware::biometrics::face;
+using namespace ::aidl::android::hardware::keymaster;
+
+namespace aidl::android::hardware::biometrics::face {
+
+class TestSessionCallback : public BnSessionCallback {
+ public:
+ ndk::ScopedAStatus onChallengeGenerated(int64_t challenge) override {
+ mLastChallenge = challenge;
+ return ndk::ScopedAStatus::ok();
+ };
+ ::ndk::ScopedAStatus onChallengeRevoked(int64_t challenge) override {
+ mLastChallengeRevoked = challenge;
+ return ndk::ScopedAStatus::ok();
+ };
+ ::ndk::ScopedAStatus onError(Error error, int32_t) override {
+ mError = error;
+ return ndk::ScopedAStatus::ok();
+ };
+ ::ndk::ScopedAStatus onEnrollmentProgress(int32_t enrollmentId, int32_t remaining) override {
+ if (remaining == 0) mLastEnrolled = enrollmentId;
+ return ndk::ScopedAStatus::ok();
+ };
+
+ ::ndk::ScopedAStatus onAuthenticationSucceeded(int32_t enrollmentId,
+ const HardwareAuthToken&) override {
+ mLastAuthenticated = enrollmentId;
+ mAuthenticateFailed = false;
+ return ndk::ScopedAStatus::ok();
+ };
+ ::ndk::ScopedAStatus onAuthenticationFailed() override {
+ mLastAuthenticated = 0;
+ mAuthenticateFailed = true;
+ return ndk::ScopedAStatus::ok();
+ };
+ ::ndk::ScopedAStatus onInteractionDetected() override {
+ mInteractionDetectedCount++;
+ return ndk::ScopedAStatus::ok();
+ };
+
+ ::ndk::ScopedAStatus onEnrollmentFrame(const EnrollmentFrame& frame) override {
+ mEnrollmentFrames.push_back(frame.data.vendorCode);
+ return ndk::ScopedAStatus::ok();
+ }
+
+ ::ndk::ScopedAStatus onEnrollmentsEnumerated(
+ const std::vector<int32_t>& enrollmentIds) override {
+ mLastEnrollmentsEnumerated = enrollmentIds;
+ return ndk::ScopedAStatus::ok();
+ };
+ ::ndk::ScopedAStatus onEnrollmentsRemoved(const std::vector<int32_t>& enrollmentIds) override {
+ mLastEnrollmentRemoved = enrollmentIds;
+ return ndk::ScopedAStatus::ok();
+ };
+ ::ndk::ScopedAStatus onAuthenticatorIdRetrieved(int64_t authenticatorId) override {
+ mLastAuthenticatorId = authenticatorId;
+ return ndk::ScopedAStatus::ok();
+ };
+ ::ndk::ScopedAStatus onAuthenticatorIdInvalidated(int64_t authenticatorId) override {
+ mLastAuthenticatorId = authenticatorId;
+ mAuthenticatorIdInvalidated = true;
+ return ndk::ScopedAStatus::ok();
+ };
+ ::ndk::ScopedAStatus onAuthenticationFrame(const AuthenticationFrame& /*authFrame*/) override {
+ return ndk::ScopedAStatus::ok();
+ }
+ ::ndk::ScopedAStatus onLockoutPermanent() override {
+ mLockoutPermanent = true;
+ return ndk::ScopedAStatus::ok();
+ };
+ ::ndk::ScopedAStatus onLockoutTimed(int64_t /* timeout */) override {
+ return ndk::ScopedAStatus::ok();
+ }
+ ::ndk::ScopedAStatus onLockoutCleared() override {
+ mLockoutPermanent = false;
+ return ndk::ScopedAStatus::ok();
+ }
+ ::ndk::ScopedAStatus onSessionClosed() override { return ndk::ScopedAStatus::ok(); }
+
+ ::ndk::ScopedAStatus onFeaturesRetrieved(const std::vector<Feature>& features) override {
+ mFeatures = features;
+ return ndk::ScopedAStatus::ok();
+ }
+
+ ::ndk::ScopedAStatus onFeatureSet(Feature feature) override {
+ mLastFeatureSet = feature;
+ return ndk::ScopedAStatus::ok();
+ }
+
+ Error mError = Error::UNKNOWN;
+ int64_t mLastChallenge = -1;
+ int64_t mLastChallengeRevoked = -1;
+ int32_t mLastEnrolled = -1;
+ int32_t mLastAuthenticated = -1;
+ int64_t mLastAuthenticatorId = -1;
+ std::vector<int32_t> mLastEnrollmentsEnumerated;
+ std::vector<int32_t> mLastEnrollmentRemoved;
+ std::vector<Feature> mFeatures;
+ Feature mLastFeatureSet;
+ std::vector<int32_t> mEnrollmentFrames;
+ bool mAuthenticateFailed = false;
+ bool mAuthenticatorIdInvalidated = false;
+ bool mLockoutPermanent = false;
+ int mInteractionDetectedCount = 0;
+};
+
+class FakeFaceEngineTest : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ LOG(ERROR) << "JRM SETUP";
+ mCallback = ndk::SharedRefBase::make<TestSessionCallback>();
+ FaceHalProperties::enrollments({});
+ FaceHalProperties::challenge({});
+ FaceHalProperties::features({});
+ FaceHalProperties::authenticator_id({});
+ FaceHalProperties::strength("");
+ }
+
+ FakeFaceEngine mEngine;
+ std::shared_ptr<TestSessionCallback> mCallback;
+ std::promise<void> mCancel;
+};
+
+TEST_F(FakeFaceEngineTest, one_eq_one) {
+ ASSERT_EQ(1, 1);
+}
+
+TEST_F(FakeFaceEngineTest, GenerateChallenge) {
+ mEngine.generateChallengeImpl(mCallback.get());
+ ASSERT_EQ(FaceHalProperties::challenge().value(), mCallback->mLastChallenge);
+}
+
+TEST_F(FakeFaceEngineTest, RevokeChallenge) {
+ auto challenge = FaceHalProperties::challenge().value_or(10);
+ mEngine.revokeChallengeImpl(mCallback.get(), challenge);
+ ASSERT_FALSE(FaceHalProperties::challenge().has_value());
+ ASSERT_EQ(challenge, mCallback->mLastChallengeRevoked);
+}
+
+TEST_F(FakeFaceEngineTest, ResetLockout) {
+ FaceHalProperties::lockout(true);
+ mEngine.resetLockoutImpl(mCallback.get(), {});
+ ASSERT_FALSE(mCallback->mLockoutPermanent);
+ ASSERT_FALSE(FaceHalProperties::lockout().value_or(true));
+}
+
+TEST_F(FakeFaceEngineTest, AuthenticatorId) {
+ FaceHalProperties::authenticator_id(50);
+ mEngine.getAuthenticatorIdImpl(mCallback.get());
+ ASSERT_EQ(50, mCallback->mLastAuthenticatorId);
+ ASSERT_FALSE(mCallback->mAuthenticatorIdInvalidated);
+}
+
+TEST_F(FakeFaceEngineTest, GetAuthenticatorIdWeakReturnsZero) {
+ FaceHalProperties::strength("weak");
+ FaceHalProperties::authenticator_id(500);
+ mEngine.getAuthenticatorIdImpl(mCallback.get());
+ ASSERT_EQ(0, mCallback->mLastAuthenticatorId);
+ ASSERT_FALSE(mCallback->mAuthenticatorIdInvalidated);
+}
+
+TEST_F(FakeFaceEngineTest, AuthenticatorIdInvalidate) {
+ FaceHalProperties::authenticator_id(500);
+ mEngine.invalidateAuthenticatorIdImpl(mCallback.get());
+ ASSERT_NE(500, FaceHalProperties::authenticator_id().value());
+ ASSERT_TRUE(mCallback->mAuthenticatorIdInvalidated);
+}
+
+TEST_F(FakeFaceEngineTest, Enroll) {
+ FaceHalProperties::next_enrollment("1,0:30:true,1:0:true,2:0:true,3:0:true,4:0:true");
+ keymaster::HardwareAuthToken hat{.mac = {2, 4}};
+ mEngine.enrollImpl(mCallback.get(), hat, {} /*enrollmentType*/, {} /*features*/,
+ mCancel.get_future());
+ ASSERT_FALSE(FaceHalProperties::next_enrollment().has_value());
+ ASSERT_EQ(1, FaceHalProperties::enrollments().size());
+ ASSERT_EQ(1, FaceHalProperties::enrollments()[0].value());
+ ASSERT_EQ(1, mCallback->mLastEnrolled);
+}
+
+TEST_F(FakeFaceEngineTest, EnrollFails) {
+ FaceHalProperties::next_enrollment("1,0:30:true,1:0:true,2:0:true,3:0:true,4:0:false");
+ keymaster::HardwareAuthToken hat{.mac = {2, 4}};
+ mEngine.enrollImpl(mCallback.get(), hat, {} /*enrollmentType*/, {} /*features*/,
+ mCancel.get_future());
+ ASSERT_FALSE(FaceHalProperties::next_enrollment().has_value());
+ ASSERT_EQ(0, FaceHalProperties::enrollments().size());
+}
+
+TEST_F(FakeFaceEngineTest, EnrollCancel) {
+ FaceHalProperties::next_enrollment("1,0:30:true,1:0:true,2:0:true,3:0:true,4:0:false");
+ keymaster::HardwareAuthToken hat{.mac = {2, 4}};
+ mCancel.set_value();
+ mEngine.enrollImpl(mCallback.get(), hat, {} /*enrollmentType*/, {} /*features*/,
+ mCancel.get_future());
+ ASSERT_EQ(Error::CANCELED, mCallback->mError);
+ ASSERT_EQ(-1, mCallback->mLastEnrolled);
+ ASSERT_EQ(0, FaceHalProperties::enrollments().size());
+ ASSERT_FALSE(FaceHalProperties::next_enrollment().has_value());
+}
+
+TEST_F(FakeFaceEngineTest, Authenticate) {
+ FaceHalProperties::enrollments({100});
+ FaceHalProperties::enrollment_hit(100);
+ mEngine.authenticateImpl(mCallback.get(), 0 /* operationId*/, mCancel.get_future());
+
+ ASSERT_EQ(100, mCallback->mLastAuthenticated);
+ ASSERT_FALSE(mCallback->mAuthenticateFailed);
+}
+
+TEST_F(FakeFaceEngineTest, AuthenticateCancel) {
+ FaceHalProperties::enrollments({100});
+ FaceHalProperties::enrollment_hit(100);
+ mCancel.set_value();
+ mEngine.authenticateImpl(mCallback.get(), 0 /* operationId*/, mCancel.get_future());
+ ASSERT_EQ(Error::CANCELED, mCallback->mError);
+}
+
+TEST_F(FakeFaceEngineTest, AuthenticateFailedForUnEnrolled) {
+ FaceHalProperties::enrollments({3});
+ FaceHalProperties::enrollment_hit(100);
+ mEngine.authenticateImpl(mCallback.get(), 0 /* operationId*/, mCancel.get_future());
+ ASSERT_EQ(Error::UNABLE_TO_PROCESS, mCallback->mError);
+ ASSERT_TRUE(mCallback->mAuthenticateFailed);
+}
+
+TEST_F(FakeFaceEngineTest, DetectInteraction) {
+ FaceHalProperties::enrollments({100});
+ FaceHalProperties::enrollment_hit(100);
+ ASSERT_EQ(0, mCallback->mInteractionDetectedCount);
+ mEngine.detectInteractionImpl(mCallback.get(), mCancel.get_future());
+ ASSERT_EQ(1, mCallback->mInteractionDetectedCount);
+}
+
+TEST_F(FakeFaceEngineTest, DetectInteractionCancel) {
+ FaceHalProperties::enrollments({100});
+ FaceHalProperties::enrollment_hit(100);
+ mCancel.set_value();
+ mEngine.detectInteractionImpl(mCallback.get(), mCancel.get_future());
+ ASSERT_EQ(Error::CANCELED, mCallback->mError);
+}
+
+TEST_F(FakeFaceEngineTest, GetFeatureEmpty) {
+ mEngine.getFeaturesImpl(mCallback.get());
+ ASSERT_TRUE(mCallback->mFeatures.empty());
+}
+
+TEST_F(FakeFaceEngineTest, SetFeature) {
+ FaceHalProperties::enrollments({1});
+ keymaster::HardwareAuthToken hat{.mac = {2, 4}};
+ mEngine.setFeatureImpl(mCallback.get(), hat, Feature::REQUIRE_ATTENTION, true);
+ auto features = mCallback->mFeatures;
+ ASSERT_TRUE(features.empty());
+ ASSERT_EQ(Feature::REQUIRE_ATTENTION, mCallback->mLastFeatureSet);
+
+ mEngine.getFeaturesImpl(mCallback.get());
+ features = mCallback->mFeatures;
+ ASSERT_FALSE(features.empty());
+ ASSERT_NE(features.end(),
+ std::find(features.begin(), features.end(), Feature::REQUIRE_ATTENTION));
+}
+
+TEST_F(FakeFaceEngineTest, ToggleFeature) {
+ FaceHalProperties::enrollments({1});
+ keymaster::HardwareAuthToken hat{.mac = {2, 4}};
+ mEngine.setFeatureImpl(mCallback.get(), hat, Feature::REQUIRE_ATTENTION, true);
+ mEngine.getFeaturesImpl(mCallback.get());
+ auto features = mCallback->mFeatures;
+ ASSERT_FALSE(features.empty());
+ ASSERT_NE(features.end(),
+ std::find(features.begin(), features.end(), Feature::REQUIRE_ATTENTION));
+
+ mEngine.setFeatureImpl(mCallback.get(), hat, Feature::REQUIRE_ATTENTION, false);
+ mEngine.getFeaturesImpl(mCallback.get());
+ features = mCallback->mFeatures;
+ ASSERT_TRUE(features.empty());
+}
+
+TEST_F(FakeFaceEngineTest, TurningOffNonExistentFeatureDoesNothing) {
+ FaceHalProperties::enrollments({1});
+ keymaster::HardwareAuthToken hat{.mac = {2, 4}};
+ mEngine.setFeatureImpl(mCallback.get(), hat, Feature::REQUIRE_ATTENTION, false);
+ mEngine.getFeaturesImpl(mCallback.get());
+ auto features = mCallback->mFeatures;
+ ASSERT_TRUE(features.empty());
+}
+
+TEST_F(FakeFaceEngineTest, SetMultipleFeatures) {
+ FaceHalProperties::enrollments({1});
+ keymaster::HardwareAuthToken hat{.mac = {2, 4}};
+ mEngine.setFeatureImpl(mCallback.get(), hat, Feature::REQUIRE_ATTENTION, true);
+ mEngine.setFeatureImpl(mCallback.get(), hat, Feature::REQUIRE_DIVERSE_POSES, true);
+ mEngine.setFeatureImpl(mCallback.get(), hat, Feature::DEBUG, true);
+ mEngine.getFeaturesImpl(mCallback.get());
+ auto features = mCallback->mFeatures;
+ ASSERT_EQ(3, features.size());
+ ASSERT_NE(features.end(),
+ std::find(features.begin(), features.end(), Feature::REQUIRE_ATTENTION));
+ ASSERT_NE(features.end(),
+ std::find(features.begin(), features.end(), Feature::REQUIRE_DIVERSE_POSES));
+ ASSERT_NE(features.end(), std::find(features.begin(), features.end(), Feature::DEBUG));
+}
+
+TEST_F(FakeFaceEngineTest, SetMultipleFeaturesAndTurnOffSome) {
+ FaceHalProperties::enrollments({1});
+ keymaster::HardwareAuthToken hat{.mac = {2, 4}};
+ mEngine.setFeatureImpl(mCallback.get(), hat, Feature::REQUIRE_ATTENTION, true);
+ mEngine.setFeatureImpl(mCallback.get(), hat, Feature::REQUIRE_DIVERSE_POSES, true);
+ mEngine.setFeatureImpl(mCallback.get(), hat, Feature::DEBUG, true);
+ mEngine.setFeatureImpl(mCallback.get(), hat, Feature::DEBUG, false);
+ mEngine.getFeaturesImpl(mCallback.get());
+ auto features = mCallback->mFeatures;
+ ASSERT_EQ(2, features.size());
+ ASSERT_NE(features.end(),
+ std::find(features.begin(), features.end(), Feature::REQUIRE_ATTENTION));
+ ASSERT_NE(features.end(),
+ std::find(features.begin(), features.end(), Feature::REQUIRE_DIVERSE_POSES));
+ ASSERT_EQ(features.end(), std::find(features.begin(), features.end(), Feature::DEBUG));
+}
+
+TEST_F(FakeFaceEngineTest, Enumerate) {
+ FaceHalProperties::enrollments({120, 3});
+ mEngine.enumerateEnrollmentsImpl(mCallback.get());
+ auto enrolls = mCallback->mLastEnrollmentsEnumerated;
+ ASSERT_FALSE(enrolls.empty());
+ ASSERT_NE(enrolls.end(), std::find(enrolls.begin(), enrolls.end(), 120));
+ ASSERT_NE(enrolls.end(), std::find(enrolls.begin(), enrolls.end(), 3));
+}
+
+TEST_F(FakeFaceEngineTest, RemoveEnrollments) {
+ FaceHalProperties::enrollments({120, 3, 100});
+ mEngine.removeEnrollmentsImpl(mCallback.get(), {120, 100});
+ mEngine.enumerateEnrollmentsImpl(mCallback.get());
+ auto enrolls = mCallback->mLastEnrollmentsEnumerated;
+ ASSERT_FALSE(enrolls.empty());
+ ASSERT_EQ(enrolls.end(), std::find(enrolls.begin(), enrolls.end(), 120));
+ ASSERT_NE(enrolls.end(), std::find(enrolls.begin(), enrolls.end(), 3));
+ ASSERT_EQ(enrolls.end(), std::find(enrolls.begin(), enrolls.end(), 100));
+}
+
+TEST_F(FakeFaceEngineTest, ResetLockoutWithAuth) {
+ FaceHalProperties::lockout(true);
+ FaceHalProperties::enrollments({33});
+ FaceHalProperties::enrollment_hit(33);
+ auto cancelFuture = mCancel.get_future();
+ mEngine.authenticateImpl(mCallback.get(), 0 /* operationId*/, cancelFuture);
+
+ ASSERT_TRUE(mCallback->mLockoutPermanent);
+
+ mEngine.resetLockoutImpl(mCallback.get(), {} /* hat */);
+ ASSERT_FALSE(mCallback->mLockoutPermanent);
+ FaceHalProperties::enrollment_hit(33);
+ mEngine.authenticateImpl(mCallback.get(), 0 /* operationId*/, cancelFuture);
+ ASSERT_EQ(33, mCallback->mLastAuthenticated);
+ ASSERT_FALSE(mCallback->mAuthenticateFailed);
+}
+
+} // namespace aidl::android::hardware::biometrics::face
\ No newline at end of file
diff --git a/biometrics/fingerprint/aidl/default/FakeFingerprintEngine.cpp b/biometrics/fingerprint/aidl/default/FakeFingerprintEngine.cpp
index d0250af..138caa0 100644
--- a/biometrics/fingerprint/aidl/default/FakeFingerprintEngine.cpp
+++ b/biometrics/fingerprint/aidl/default/FakeFingerprintEngine.cpp
@@ -19,51 +19,11 @@
#include <android-base/logging.h>
#include <fingerprint.sysprop.h>
-#include <chrono>
-#include <regex>
-#include <thread>
#include "util/CancellationSignal.h"
-
-#define SLEEP_MS(x) \
- if (x > 0) std::this_thread::sleep_for(std::chrono::milliseconds(x))
-#define BEGIN_OP(x) \
- do { \
- LOG(INFO) << __func__; \
- SLEEP_MS(x); \
- } while (0)
-#define IS_TRUE(x) ((x == "1") || (x == "true"))
-
-// This is for non-test situations, such as casual cuttlefish users, that don't
-// set an explicit value.
-// Some operations (i.e. enroll, authenticate) will be executed in tight loops
-// by parts of the UI or fail if there is no latency. For example, the
-// fingerprint settings page constantly runs auth and the enrollment UI uses a
-// cancel/restart cycle that requires some latency while the activities change.
-#define DEFAULT_LATENCY 2000
+#include "util/Util.h"
using namespace ::android::fingerprint::virt;
-using namespace ::aidl::android::hardware::biometrics::fingerprint;
-
-int64_t getSystemNanoTime() {
- timespec now;
- clock_gettime(CLOCK_MONOTONIC, &now);
- return now.tv_sec * 1000000000LL + now.tv_nsec;
-}
-
-bool hasElapsed(int64_t start, int64_t durationMillis) {
- auto now = getSystemNanoTime();
- if (now < start) return true;
- if (durationMillis <= 0) return true;
- return ((now - start) / 1000000LL) > durationMillis;
-}
-
-std::vector<std::string> split(const std::string& str, const std::string& sep) {
- std::regex regex(sep);
- std::vector<std::string> parts(std::sregex_token_iterator(str.begin(), str.end(), regex, -1),
- std::sregex_token_iterator());
- return parts;
-}
namespace aidl::android::hardware::biometrics::fingerprint {
@@ -101,14 +61,14 @@
// format is "<id>:<progress_ms>,<progress_ms>,...:<result>
auto nextEnroll = FingerprintHalProperties::next_enrollment().value_or("");
- auto parts = split(nextEnroll, ":");
+ auto parts = Util::split(nextEnroll, ":");
if (parts.size() != 3) {
LOG(ERROR) << "Fail: invalid next_enrollment";
cb->onError(Error::VENDOR, 0 /* vendorError */);
return;
}
auto enrollmentId = std::stoi(parts[0]);
- auto progress = split(parts[1], ",");
+ auto progress = Util::split(parts[1], ",");
for (size_t i = 0; i < progress.size(); i++) {
auto left = progress.size() - i - 1;
SLEEP_MS(std::stoi(progress[i]));
@@ -141,7 +101,7 @@
const std::future<void>& cancel) {
BEGIN_OP(FingerprintHalProperties::operation_authenticate_latency().value_or(DEFAULT_LATENCY));
- auto now = getSystemNanoTime();
+ auto now = Util::getSystemNanoTime();
int64_t duration = FingerprintHalProperties::operation_authenticate_duration().value_or(0);
do {
if (FingerprintHalProperties::operation_authenticate_fails().value_or(false)) {
@@ -172,7 +132,7 @@
}
SLEEP_MS(100);
- } while (!hasElapsed(now, duration));
+ } while (!Util::hasElapsed(now, duration));
LOG(ERROR) << "Fail: not enrolled";
cb->onAuthenticationFailed();