Alaqan Mini SDK
для Android
Java SDK для сканера ладони Alaqan Mini. Поставляется в виде AAR; работает через USB Host на Android 7.0 и выше.
Обзор
Три операции: startIdentify(), startEnroll(), startRescan(). Два реестра слушателей: DeviceEventsListener для состояния устройства и FrameListener для обработанных кадров. Оба привязываются к вашему LifecycleOwner. SDK берёт на себя USB-сессию, поток с двух камер и инференс на устройстве. От приложения требуется UI и API-ключ.
Публичный интерфейс
// Подготовка
sdk.setApiKey(String);
sdk.initialize(Context);
sdk.addDeviceEventsListener(LifecycleOwner, DeviceEventsListener);
sdk.addFrameListener(LifecycleOwner, FrameListener); // для превью, по необходимости
// Операции (только одна одновременно)
sdk.startIdentify(IdentifyCallback);
sdk.startEnroll(externalId, name, metadata, EnrollCallback);
sdk.startRescan(externalId, RescanCallback);
sdk.stopOperation();
// Устройство
sdk.getDevice().connect() / disconnect() / reboot();
// Завершение работы (при выходе из процесса)
sdk.destroy();
Требования
| Параметр | Значение |
|---|---|
| Android API | 24+ (рекомендуется target 34) |
| ABI | arm64-v8a, armeabi-v7a |
| Размер AAR | ~21 МБ |
| Расход RAM | ~30 МБ нативной памяти в установившемся режиме |
| Аппаратные требования | USB Host (OTG); сканер Alaqan Mini |
| Сеть | Исходящий HTTPS для активации и биометрических вызовов |
| Разрешения | INTERNET (объявляется самим SDK); фича android.hardware.usb.host |
Установка
Положите AAR в app/libs/
app/
├── libs/
│ └── alaqan-mini-sdk.aar
└── build.gradle.kts
Подключите в build.gradle.kts
android {
defaultConfig {
minSdk = 24
ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a") }
}
}
dependencies {
implementation(files("libs/alaqan-mini-sdk.aar"))
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.lifecycle:lifecycle-common:2.6.2")
}
USB device-фильтр (res/xml/device_filter.xml)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<usb-device vendor-id="17992" product-id="65280" />
</resources>
AndroidManifest.xml: минимальная настройка активности
<uses-feature android:name="android.hardware.usb.host" android:required="true" />
<activity android:name=".MainActivity"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter" />
</activity>
singleTask?
USB-attach-интент из манифеста придёт в уже работающий экземпляр через onNewIntent(), а не запустит новую активность. USB-сессия SDK при этом не разрывается.
Быстрый старт
Один экземпляр AlaqanMiniSDK на процесс. Храните его в Application. Launcher-активность инициализирует SDK и открывает USB-сессию; дальше вы активируете UI по состоянию подключения. Каждая операция живёт в своей активности (см. раздел Идентификация).
// App.java: единственный экземпляр SDK, общий для всех активностей.
public final class App extends Application {
private final AlaqanMiniSDK sdk = new AlaqanMiniSDK();
public AlaqanMiniSDK getSdk() { return sdk; }
}
// MainActivity.java: поднимает SDK и реагирует на его события.
public class MainActivity extends AppCompatActivity
implements DeviceEventsListener {
private AlaqanMiniSDK sdk;
@Override
protected void onCreate(Bundle b) {
super.onCreate(b);
sdk = ((App) getApplication()).getSdk();
sdk.setApiKey(BuildConfig.ALAQAN_API_KEY);
sdk.addDeviceEventsListener(this, this); // удалится автоматически при ON_DESTROY
sdk.initialize(this);
}
@Override public void onInitialized() { sdk.getDevice().connect(); }
@Override public void onDeviceConnected(@NonNull Device d) { /* откройте доступ к операциям в UI */ }
@Override public void onDeviceDisconnected(@NonNull Device d) { /* закройте доступ к операциям в UI */ }
@Override public void onError(@NonNull SdkError e) { /* только фоновые ошибки; ошибки операций приходят в её колбэк */ }
@Override
protected void onDestroy() {
if (isFinishing()) sdk.destroy();
super.onDestroy();
}
}
Подставляйте ключ через BuildConfig, получайте от своего бэкенда при первом запуске или храните в защищённом хранилище. SDK ключ нигде не сохраняет.
onDeviceConnected.
Это событие срабатывает на каждом переподключении (дёрнули кабель, мягкая перезагрузка). Запускайте операции в ответ на действие пользователя, обычно в onResume отдельного экрана.
Модель слушателей
Два реестра, оба поддерживают несколько подписчиков и привязку к жизненному циклу. Регистрируйте слушатель один раз на каждый LifecycleOwner; SDK снимет его при ON_DESTROY.
| Реестр | Что доставляет | Частота | Привязка к жизненному циклу |
|---|---|---|---|
DeviceEventsListener | Готовность SDK, подключение/отключение устройства, фоновые ошибки | Редко | Снимается при ON_DESTROY |
FrameListener | FrameResult на каждый кадр | ~30 Гц | Активен только между ON_START и ON_STOP |
Повторная доставка состояния
Если onInitialized() или onDeviceConnected() уже произошли до подписки, SDK доставит их заново новому слушателю. Активность, пересозданная после поворота, переподпишется и получит актуальное состояние без опроса isInitialized() / isConnected().
Автоматический стриминг
Поток кадров запускается, когда добавлен первый FrameListener, и останавливается после удаления последнего. Методов startStreaming() / stopStreaming() нет; переключателем служит сама регистрация.
Где появляются ошибки
| Источник | Куда приходит |
|---|---|
| Во время активной операции | В onResult колбэка операции |
| Вне операций (инициализация, USB, watchdog) | В DeviceEventsListener.onError |
Ошибки никогда не приходят дважды. За исход операции отвечает её собственный колбэк.
Потоки
Все колбэки вызываются в главном потоке. Публичные методы SDK можно вызывать из любого потока. Единственное ограничение: Device.getSensingRange() ожидает ответа по USB, поэтому вызывайте его из фонового потока.
stopOperation() потокобезопасен. Терминальный onResult(OP_CANCELLED) всегда приходит в главном потоке; при вызове из главного потока он срабатывает синхронно, до возврата из вашего обработчика.
Жизненный цикл
Один экземпляр SDK на процесс; жизненный цикл каждой активности привязывает свои слушатели.
| Где | Что вызывать |
|---|---|
Application.onCreate() | new AlaqanMiniSDK() и держать в Application |
onCreate() первой активности | setApiKey(...) · addDeviceEventsListener(this, this) · initialize(this) |
onCreate() любой активности | addDeviceEventsListener(this, this). Повтор sticky-состояния синхронизирует UI. |
onCreate() экрана с превью | addFrameListener(this, this). Стрим идёт только в состоянии STARTED. |
onDestroy() launcher-активности при isFinishing() | destroy() |
destroy() можно вызвать только один раз.
После destroy() повторный initialize() на том же экземпляре выбрасывает IllegalStateException. Чтобы инициализироваться заново, создайте новый AlaqanMiniSDK. Не вызывайте destroy() в onDestroy() любой активности, кроме launcher: при этом выгружаются модели и следующий запуск будет «холодным».
Операции
Три операции; одновременно может выполняться только одна. Запуск второй, пока первая не завершилась, немедленно возвращает OP_ALREADY_RUNNING в колбэк новой операции.
void startIdentify(IdentifyCallback cb);
void startEnroll(String externalId, String name,
@Nullable String metadata, EnrollCallback cb);
void startRescan(String externalId, RescanCallback cb);
void stopOperation();
void setOperationTimeout(long ms); // по умолчанию 30 000; 0 отключает таймаут
OperationState getOperationState(); // IDLE | DETECTING | CAPTURING | PROCESSING
OperationType getOperationType(); // NONE | IDENTIFY | ENROLL | RESCAN
Состояния
| Состояние | Что означает |
|---|---|
IDLE | Активной операции нет |
DETECTING | Ожидание руки, проходящей пороги уверенности, качества и стабильности |
CAPTURING | Только enroll/rescan: набираются три кадра |
PROCESSING | Кадры захвачены, идёт обращение к серверу |
Идентификация (1:N)
Захватывает один стабильный кадр и отправляет его на сервер. В результате приходит найденный пользователь или ошибка. Запускайте в onResume, отменяйте в onPause; активность владеет временем жизни операции.
// IdentifyActivity.java: один цикл identify, привязан к onResume/onPause.
public class IdentifyActivity extends AppCompatActivity
implements IdentifyCallback {
private AlaqanMiniSDK sdk;
@Override
protected void onCreate(Bundle b) {
super.onCreate(b);
sdk = ((App) getApplication()).getSdk();
}
@Override protected void onResume() { super.onResume(); sdk.startIdentify(this); }
@Override protected void onPause() { sdk.stopOperation(); super.onPause(); }
@Override public void onPreviewFrame(@NonNull FrameResult r) {
// r.bitmap общий буфер. Отрисуйте здесь. Ссылку не сохраняйте.
}
@Override public void onGuidance(@NonNull Guidance g) {
// сопоставьте g со своими локализованными строками; SDK сам не локализует.
}
@Override public void onResult(@NonNull IdentifyResult r) {
if (isFinishing() || isDestroyed()) return;
ErrorCode code = r.error != null ? r.error.code : null;
if (code == ErrorCode.OP_CANCELLED) return; // пользователь ушёл с экрана
if (code == ErrorCode.OP_DISCONNECTED
|| code == ErrorCode.OP_NOT_CONNECTED) { finish(); return; }
// r.isSuccess() → r.externalId, r.name, r.metadata
// иначе → r.error
finish();
}
}
| Поле IdentifyResult | Тип | Комментарий |
|---|---|---|
scanId | String? | Идентификатор скана на сервере; только при успехе |
externalId | String? | External ID найденного пользователя |
name | String? | null, если имя не задавалось при регистрации |
metadata | String? | JSON-строка, заданная при регистрации |
error | SdkError? | null при успехе |
Регистрация
Захватывает три стабильных кадра и отправляет на сервер тот, у которого лучшее качество. onProgress срабатывает после каждого захвата, удобно для индикатора N/3.
sdk.startEnroll("emp-1024", "Айман", null, new EnrollCallback() {
@Override public void onGuidance(@NonNull Guidance g) { /* обновите подсказку */ }
@Override public void onProgress(int captured, int total) { /* отрисуйте N/total */ }
@Override public void onResult(@NonNull EnrollResult r) {
// r.isSuccess() → r.scanId; иначе r.error.
}
});
nameиmetadataнеобязательны; передавайтеnull, чтобы пропустить.metadata, если задано, должно быть валидной JSON-строкой объекта.EnrollResultсодержит поляscanIdиerror.
Повторное сканирование
Заменяет биометрический шаблон уже зарегистрированного пользователя, сохраняя его name и metadata. Поведение и тип результата те же, что у регистрации. Интерфейс-маркер RescanCallback extends EnrollCallback делает намерение явным в месте вызова.
sdk.startRescan("emp-1024", new RescanCallback() {
@Override public void onGuidance(@NonNull Guidance g) { /* обновите подсказку */ }
@Override public void onProgress(int captured, int total) { /* отрисуйте N/total */ }
@Override public void onResult(@NonNull EnrollResult r) {
// r.isSuccess() при успехе; иначе r.error.
}
});
Подсказки
UI-подсказки, приходящие при смене состояния. Сопоставляйте каждое значение со своими локализованными строками; SDK сам не локализует.
| Значение | Когда |
|---|---|
SHOW_PALM | В кадре нет руки |
MOVE_CLOSER | Площадь ограничивающей рамки руки ниже порога |
MOVE_FURTHER | Площадь ограничивающей рамки руки выше порога |
CENTER_PALM | Рамка смещена от центра кадра |
HOLD_STEADY | Рука найдена, но не пройдены пороги качества или стабильности |
CAPTURING | Только enroll/rescan; срабатывает на каждый захваченный кадр |
PROCESSING | Идёт обращение к серверу |
Справочник API
AlaqanMiniSDK
void setApiKey(@Nullable String key);
void initialize(@NonNull Context ctx);
boolean isInitialized();
Device getDevice();
@Nullable String getNativeVersion();
void setLedGateEnabled(boolean on); // по умолчанию true
void setConfig(@NonNull SdkConfig cfg);
SdkConfig getConfig();
void destroy();
| Метод | Контракт |
|---|---|
setApiKey | Должен быть вызван до initialize(). Установка null после инициализации ведёт к ошибкам API_KEY_MISSING на серверных вызовах, пока не будет задан новый ключ. |
initialize | Асинхронный. При успехе вызывает onInitialized(). При ошибке вызывает onError(SdkError) с одним из API_KEY_MISSING / SERVER_NETWORK_ERROR / MODEL_LOAD_FAILED. Идемпотентен, пока не вызван destroy(). |
setLedGateEnabled | При true кадры с выключенным LED устройства не передаются на инференс. Установите false для хорошо освещённых стационарных установок, где LED не зажигается. |
destroy | Освобождает нативную память, USB-сессию и потоки. Вызывается на экземпляр один раз. |
Device
// Соединение
void connect(); // до инициализации SDK ничего не делает
void disconnect();
boolean isConnected();
// Идентификация устройства
@Nullable String getSerialNumber(); // напр. "GX4TYQWH3D"
@Nullable String getFirmwareVersion(); // напр. "2026042319"
// Датчик расстояния
int getDistance(); // последнее измерение в мм; -1, если недоступно
@Nullable int[] getSensingRange(); // {min,max} в мм. БЛОКИРУЮЩИЙ ВЫЗОВ.
void setSensingRange(int min, int max); // асинхронно; по умолчанию 50–200; ограничен [0,255]
// Камеры
Cameras cameras();
// Питание
void reboot(); // мягкая перезагрузка: отключение и автоподключение
Все геттеры возвращают null или -1, пока не сработает onDeviceConnected. setSensingRange работает «отправил и забыл». getSensingRange ожидает ответа по USB; вызывайте его из фонового потока.
Cameras
У сканера две камеры: CameraType.IR (инфракрасная) и CameraType.VL (видимый свет). Активная камера служит источником превью, доставляемого в FrameListener.
Cameras cams = sdk.getDevice().cameras();
cams.setActive(CameraType.VL); // сменить источник превью
CameraType active = cams.getActiveType();
long ir = cams.getIrFrameCount(); // кадры с момента подключения
long vl = cams.getVlFrameCount();
Камеру детекции SDK выбирает сам на время операции; партнёрскому коду она не доступна.
SdkConfig
Сейчас доступен один параметр: интервал срабатывания watchdog при отсутствии кадров. Применяется через setConfig(); начинает действовать при следующем запуске watchdog.
sdk.setConfig(new SdkConfig.Builder()
.frameWatchdogMs(15_000L) // по умолчанию 30 000; минимум 1 000
.build());
Если устройство подключено, но за frameWatchdogMs мс не пришло ни одного кадра, SDK однократно выдаёт DEVICE_NOT_RESPONDING в onError. Соединение при этом остаётся открытым; это сигнал, а не команда на завершение. Пороги качества и стабильности партнёром не настраиваются.
Слушатели и колбэки
DeviceEventsListener
public interface DeviceEventsListener {
default void onInitialized() {}
default void onDeviceConnected(@NonNull Device d) {}
default void onDeviceDisconnected(@NonNull Device d) {}
default void onError(@NonNull SdkError error) {}
}
sdk.addDeviceEventsListener(listener); // ручное управление временем жизни
sdk.addDeviceEventsListener(lifecycleOwner, listener); // снимается при ON_DESTROY
sdk.removeDeviceEventsListener(listener);
Все методы помечены как default; реализуйте только нужные. Ошибки активной операции сюда не приходят.
FrameListener
public interface FrameListener { void onFrame(@NonNull FrameResult r); }
sdk.addFrameListener(listener); // стрим запускается
sdk.addFrameListener(lifecycleOwner, listener); // активен только в STARTED
sdk.removeFrameListener(listener); // после удаления последнего стрим останавливается
FrameResult.bitmap используется совместно и переиспользуется. Внутри onFrame отрисуйте кадр или скопируйте его через Bitmap.copy(); не вызывайте recycle() и не сохраняйте ссылку за пределами колбэка.
IdentifyCallback
public interface IdentifyCallback {
default void onPreviewFrame(@NonNull FrameResult r) {}
void onGuidance(@NonNull Guidance g);
void onResult(@NonNull IdentifyResult r);
}
onResult срабатывает ровно один раз. onPreviewFrame по умолчанию пустой; переопределяйте его, только если рисуете превью прямо из колбэка операции. Иначе используйте отдельный FrameListener.
EnrollCallback
public interface EnrollCallback {
default void onPreviewFrame(@NonNull FrameResult r) {}
void onGuidance(@NonNull Guidance g);
void onProgress(int captured, int total); // total сейчас равен 3
void onResult(@NonNull EnrollResult r);
}
RescanCallback
public interface RescanCallback extends EnrollCallback {}
Интерфейс-маркер. Методы те же, что в EnrollCallback. Используйте его в месте вызова, чтобы намерение было видно; кроме того, SDK сможет добавить специфичные для rescan события, не сломав код, использующий EnrollCallback.
Типы данных
FrameResult
| Поле | Тип | Комментарий |
|---|---|---|
jpegSize | int | Размер исходного JPEG в байтах |
width / height | int | Размеры кадра в пикселях |
bitmap | Bitmap? | Декодированное превью. Общий буфер: не вызывайте recycle, не сохраняйте ссылку |
hands | HandDetection[]? | null, если рук не найдено |
moreHands | boolean | true, если в кадре оказалось больше рук, чем влезло в массив |
inferenceMs | float | Полное время инференса |
ledOn | boolean | Состояние LED устройства в момент захвата |
steady | boolean | Рамка стабильна между последовательными детекциями |
HandDetection
| Поле | Тип | Комментарий |
|---|---|---|
confidence | float | Уверенность детекции, 0–1 |
x / y / w / h | float | Левый верхний угол + ширина и высота рамки в пикселях |
quality | float | Оценка качества, 0–100; -1, если не вычислено |
Результаты
Оба типа результатов устроены одинаково: сначала isSuccess(); при неудаче читайте error.
| Тип | Поля | Возвращается из |
|---|---|---|
IdentifyResult | scanId, externalId, name, metadata, error | startIdentify() |
EnrollResult | scanId, error | startEnroll(), startRescan() |
SdkError
public final class SdkError {
@NonNull public final ErrorCode code;
@Nullable public final String detail;
}
В коде ветвитесь по code (enum). Поле detail содержит сообщение сервера для SERVER_REJECTED или текст исключения. Используйте его для логов, не для управляющей логики.
ErrorCode предоставляет: code() (стабильный 4-значный код для телеметрии), category() (DEVICE, AUTH, STREAMING, NATIVE, NETWORK, OPERATION) и fromCode(String).
@Override
public void onError(@NonNull SdkError e) {
switch (e.code) {
case API_KEY_MISSING: promptForKey(); break;
case DEVICE_NOT_RESPONDING:
case USB_NO_FW_VERSION: // информационное; UI не трогаем break;
default:
if (e.code.category() == ErrorCode.Category.NETWORK) showOffline();
else showError(e.code.code());
}
}
Коды ошибок
Полный enum ErrorCode. В коде ветвитесь по значению enum; code() возвращает стабильный 4-значный строковый код, удобный для логов и серверной отчётности.
Устройство и USB (1xxx)
| Константа | Что означает · что делать | |
|---|---|---|
| 1001 | USB_PERMISSION_DENIED | Пользователь отклонил системный диалог разрешения на USB. Запросите снова или подскажите, как разрешить доступ. |
| 1002 | USB_NO_INTERFACE | На устройстве нет нужного USB-интерфейса. Проверьте VID/PID-фильтр и кабель. |
| 1003 | USB_NO_ENDPOINTS | Нет требуемых USB-эндпоинтов. Аппаратная проблема: переподключите или замените устройство. |
| 1004 | USB_OPEN_FAILED | Не удалось открыть USB-устройство. Часто следует за сбоем USB-стека ядра; попробуйте переподключить. |
| 1005 | USB_CLAIM_FAILED | Интерфейс держит другой процесс. Закройте другие приложения, использующие устройство. |
| 1006 | USB_NO_SERIAL | Серийный номер прочитать не удалось. Без него невозможны серверные биометрические вызовы; обработайте как ошибку подключения. |
| 1007 | DEVICE_NOT_RESPONDING | За время frameWatchdogMs не пришло ни одного кадра. Информационное; соединение сохраняется. |
| 1008 | USB_NO_FW_VERSION | Не удалось получить версию прошивки. Информационное; соединение сохраняется. |
Аутентификация (2xxx)
| Константа | Что означает · что делать | |
|---|---|---|
| 2001 | AUTH_CHALLENGE_FAILED | Аутентификация устройства не прошла на этапе 1. Переподключите или перезагрузите устройство. |
| 2002 | AUTH_INVALID_REPLY | Устройство вернуло неожиданный ответ во время аутентификации. Переподключите или перезагрузите. |
| 2003 | AUTH_COMPUTE_FAILED | Сбой клиентского шага аутентификации. Вызовите initialize() повторно. |
| 2004 | AUTH_VERIFY_FAILED | Аутентификация устройства не прошла на этапе 2. Переподключите или перезагрузите устройство. |
| 2005 | AUTH_REJECTED | Устройство отвергло учётные данные SDK. Вызовите initialize() повторно; если ошибка повторяется, обратитесь в поддержку. |
Стриминг (3xxx)
| Константа | Что означает · что делать | |
|---|---|---|
| 3001 | STREAM_START_FAILED | Не удалось отправить команду запуска стрима. Переподключите устройство; если ошибка повторяется, обратитесь в поддержку. |
| 3002 | STREAM_REJECTED | Устройство отклонило запуск стрима. Перезагрузите устройство. |
SDK и нативная часть (4xxx)
| Константа | Что означает · что делать | |
|---|---|---|
| 4001 | NATIVE_CREATE_FAILED | Не удалось запустить нативный рантайм. Убедитесь, что упакованы обе ABI. |
| 4002 | MODEL_LOAD_FAILED | Не удалось загрузить модели. Убедитесь, что активация прошла без ошибок (нет предшествующих SERVER_*). |
| 4004 | AUTH_PROVIDER_MISSING | Внутренняя ошибка конфигурации; в релизных сборках не возникает. |
Сеть (5xxx)
| Константа | Что означает · что делать | |
|---|---|---|
| 5001 | API_KEY_MISSING | Задайте ключ через setApiKey() до initialize(). |
| 5002 | SERVER_NETWORK_ERROR | Транспортная ошибка. Покажите офлайн-UI и предложите повтор. |
| 5003 | SERVER_INVALID_RESPONSE | Некорректный ответ сервера. Соберите логи; при повторе обратитесь в поддержку. |
| 5004 | SERVER_REJECTED | Сервер вернул сообщение об ошибке. Читайте error.detail; типичные значения собраны в каталоге ниже. |
| 5005 | SERVER_TIMEOUT | Запрос превысил 15 с. Предложите повтор; проверьте качество сети. |
Операции (6xxx)
| Константа | Что означает · что делать | |
|---|---|---|
| 6001 | OP_ALREADY_RUNNING | Уже выполняется другая операция. Вызовите stopOperation() или дождитесь её завершения. |
| 6002 | OP_CANCELLED | Вы вызвали stopOperation(). В UI обработайте как пустое действие. |
| 6003 | OP_TIMEOUT | Истёк таймаут операции. Покажите UI повторной попытки. |
| 6004 | OP_DISCONNECTED | Устройство отключилось во время операции. Закройте экран. |
| 6005 | OP_NOT_INITIALIZED | Операция запущена до onInitialized(). Дождитесь события или проверьте isInitialized(). |
| 6006 | OP_NOT_CONNECTED | Операция запущена до onDeviceConnected(). Дождитесь или проверьте device.isConnected(). |
| 6007 | OP_INVALID_PARAMS | Неверные параметры. Обычно это пустой externalId у enroll/rescan. |
Каталог SERVER_REJECTED.detail
Когда code == SERVER_REJECTED, в error.detail лежит сообщение сервера в исходном виде. Сопоставьте их со своими UI-строками:
| detail | В каких операциях | Что показать |
|---|---|---|
"no hand detected" | любая | Попросите пользователя повторить попытку |
"quality check failed" | любая | Попросите повторить, поднеся руку чище или ближе |
"liveness check failed" | любая | Попросите повторить с настоящей ладонью |
"template not found" | identify | Покажите «не зарегистрирован» |
"person not found" | rescan | Указанный externalId не зарегистрирован; переключитесь на enroll |
"person already exists" | enroll | Спросите, не выполнить ли rescan вместо регистрации |
"template already enrolled" | enroll/rescan | Биометрия совпала с другим пользователем. Разберитесь со случаем вручную. |
"duplicate capture detected" | любая | Один и тот же снимок отправлен дважды; вероятно, двойной тап. |
"invalid inference" | любая | Не повторять. Эскалируйте в поддержку. |
"inference unavailable" | любая | Повторите запрос с экспоненциальной задержкой |
Лучшие практики
Один экземпляр SDK в Application
Создавайте один раз, используйте во всех активностях, вызывайте destroy() только в момент завершения launcher-активности.
Привязывайте слушателей к LifecycleOwner
Слушатель снимется при ON_DESTROY автоматически. FrameListener ещё и приостанавливает поток, когда экран уходит на задний план.
@Override
protected void onCreate(Bundle b) {
super.onCreate(b);
sdk.addDeviceEventsListener(this, this); // повтор sticky-состояния + автоудаление
sdk.addFrameListener(this, this); // поток только пока экран виден
}
Стройте UI вокруг onGuidance / onProgress / onResult
Отдельный FrameListener во время операции не нужен. Колбэки операций уже доставляют превью через onPreviewFrame. Выберите один источник.
Аккуратная отмена при навигации
@Override
protected void onPause() {
sdk.stopOperation(); // выдаст OP_CANCELLED; обработайте как пустое действие
super.onPause();
}
Ветвитесь по ErrorCode, а не по строке
Используйте enum и category(). Строки оставьте для логов и серверной телеметрии.
DEVICE_NOT_RESPONDING и USB_NO_FW_VERSION: информационные
Оба сигнальные, ни один не разрывает соединение. Реальные отключения приходят в onDeviceDisconnected.
Решение проблем
onDeviceConnected ни разу не срабатывает
- Проверьте кабель, порт и поддержку OTG на хосте.
- В
device_filter.xmlзаданыvendor-id="17992"иproduct-id="65280". - У активности есть USB-фильтр в манифесте и
android:launchMode="singleTask".
Инициализация падает с SERVER_NETWORK_ERROR / API_KEY_MISSING / MODEL_LOAD_FAILED
- API-ключ задан до вызова
initialize(). - Сеть доступна. Активация идёт по HTTPS с тайм-аутом 15 с.
- В APK упакованы обе ABI (
arm64-v8a,armeabi-v7a).
Подсказка застряла на HOLD_STEADY
- Рука вне настроенного диапазона; посмотрите
device.getDistance(). - На стекло сканера попадает прямой солнечный свет; защитная плёнка не снята.
- Стационарная установка с выключенным LED. Вызовите
setLedGateEnabled(false).
Кадры перестали приходить (DEVICE_NOT_RESPONDING)
- Попробуйте
device.reboot(). - Если ошибка повторяется, переподключите устройство или обратитесь в поддержку.
Поддержка
Техническая
support@alaqan.com
Продажи
sales@alaqan.com
Строку из ErrorCode.code(). Поле error.detail, если есть. Шаги воспроизведения. Серийный номер устройства и версию прошивки. Версию SDK. Logcat за период сбоя.