修复应用稳定性并发布人脸识别工程

This commit is contained in:
FinleyHsu
2026-07-22 10:37:45 +08:00
parent c11878e3f2
commit ce6bcb2f51
71 changed files with 13786 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
app/src/main/assets/face_models/*.csta filter=lfs diff=lfs merge=lfs -text
+11
View File
@@ -0,0 +1,11 @@
.DS_Store
.idea/
.gradle/
*.iml
local.properties
build/
sdk-demo/
*.idsig
*.apk
*.ap_
keystore/
+104
View File
@@ -0,0 +1,104 @@
# 星元智灵
星元智灵是基于智投终端流程改造的竖屏 Android 投放终端应用。项目保留串口投口控制、称重回包、本地备份和服务器上报框架,并集成本地 SeetaFace6 人脸识别与人员录入流程。
本项目为沃尔核材专用 APP,定位为安卓上位机应用。
## 主要功能
- 首页显示摄像头实时画面并默认进行本地人脸识别,也可通过刷卡或工号进入投放流程。
- 设置页新增“人脸”Tab,可新增人员、录入或更新人脸、查看人员列表并删除人员。
- 第二页填写本次投放信息:机台号、人员、生产批次。
- 机台号支持手动输入,也支持小窗口表格选择;选择后自动联动人员和生产批次。
- 第三页支持 1/2/3/4 投口模式,可选择单个投口或打开全部投口,页面展示本次机台号、人员、生产批次。
- 投递页支持关门倒计时、手动关门、一次性延时关门。
- 结果页仅显示本次投放垃圾类型和重量,不显示奖励。
- 投口垃圾类型支持镀锡铜、铝合金、废铜、废电线、废铝、废锡和自定义。
- 投放结果通过预留接口上报服务器,接口未定时使用本地队列和备份兜底。
- 保留下位机原始称重小数;串口或下位机不可用时不会生成虚假称重结果。
## 工程结构
```text
app/src/main/
AndroidManifest.xml Android 应用配置
assets/www/ WebView 前端界面
assets/face_models/ SeetaFace6 人脸模型
assets/brand/ App 图标源图
java/ 原生桥、串口、TCP 上报逻辑
jniLibs/armeabi-v7a/ 串口 native 库
res/mipmap-*/ Android launcher 图标
build.sh 无 Gradle 手动构建脚本
```
## 构建要求
- macOS
- Android SDK,包含:
- `platforms/android-28/android.jar`
- `build-tools/34.0.0`
- JDK 8 或兼容 `javac --release 8` 的 JDK
- Git LFS(仓库中的 SeetaFace6 `.csta` 模型通过 LFS 管理)
- Android SDK platform-tools,用于 `adb install`
## 构建 APK
```bash
bash build.sh
```
构建产物:
```text
build/out/xingyuan-zhiling-signed.apk
```
首次构建时,如果本地没有 debug keystore,脚本会自动生成:
```text
keystore/zhitou-debug.keystore
```
该 keystore 不提交到 Git。
## 安装到终端
```bash
$HOME/Library/Android/sdk/platform-tools/adb install -r build/out/xingyuan-zhiling-signed.apk
$HOME/Library/Android/sdk/platform-tools/adb shell monkey -p com.xingyuan.zhiling -c android.intent.category.LAUNCHER 1
```
## 串口说明
默认串口配置:
```text
path: /dev/ttyS4
baudRate: 9600
```
如果首页提示串口故障,请检查串口占用情况以及下位机连接。旧智投 APP 与星元智灵同时运行时,可能会竞争 `/dev/ttyS4`
## 接口说明
当前机台号、人员、生产批次接口仍为占位实现。接口未接入时页面允许手动填写机台号,不会注入测试人员。后续接入正式接口时,需要替换:
- `app/src/main/java/com/zhitou/terminal/ZhitouBridge.java` 中的 `fetchWorkContext`
- 前端 `app/src/main/assets/www/app.js` 中的接口解析和兜底逻辑
投放结果上报预留:
- `reportXingyuanDelivery`
- `reportDeliveryResult`
- 本地备份:`/sdcard/smart/cache/xingyuan_zhiling_delivery_backup.csv`
- 本地 JSON 备份:`/sdcard/smart/cache/xingyuan_zhiling_delivery_backup.json`
- 日志导出:`/sdcard/smart/logs/`
## 版本
当前应用版本:
```text
versionName: 1.2.12-wal-review-fix
versionCode: 101
package: com.xingyuan.zhiling
```
+55
View File
@@ -0,0 +1,55 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.xingyuan.zhiling"
android:versionCode="101"
android:versionName="1.2.12-wal-review-fix">
<uses-sdk
android:minSdkVersion="21"
android:targetSdkVersion="28" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<application
android:name=".XingyuanApplication"
android:allowBackup="false"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:exported="true"
android:launchMode="singleTask"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<!-- 注册为桌面 Launcher,可设为默认桌面实现开机直接进入 app -->
<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<!-- 开机自启动接收器 -->
<receiver
android:name=".BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
</application>
</manifest>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

@@ -0,0 +1,11 @@
Copyright (c) 2019, SeetaTech,
Institute of Computing Technology, Chinese Academy of Sciences, Beijing, China
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b57171c55f76f343f6f579e1ea1e165d1d4fc0321b7395f6abde155d18fe0082
size 4054932
Binary file not shown.
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3cefce29d2ceb6fd95153d67c11f24ce08746e1877478bacaa5ae5d333a0a670
size 102551956
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+16
View File
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#1b3134" />
<title>星元智灵</title>
<link rel="icon" href="data:," />
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main id="app" class="app-shell" aria-live="polite"></main>
<div id="toast" class="toast" role="status"></div>
<script src="./app.js"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+315
View File
@@ -0,0 +1,315 @@
#include <jni.h>
#include <algorithm>
#include <cmath>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include "seeta/FaceDetector.h"
#include "seeta/FaceLandmarker.h"
#include "seeta/FaceRecognizer.h"
namespace {
std::mutex g_mutex;
std::unique_ptr<seeta::FaceDetector> g_detector;
std::unique_ptr<seeta::FaceLandmarker> g_landmarker;
std::unique_ptr<seeta::FaceRecognizer> g_recognizer;
std::string g_last_error;
constexpr double kCenterLeftRatio = 0.25;
constexpr double kCenterRightRatio = 0.75;
constexpr double kCenterTopRatio = 0.18;
constexpr double kCenterBottomRatio = 0.82;
constexpr double kMaxEyeTiltRatio = 0.18;
constexpr double kMaxMouthTiltRatio = 0.22;
constexpr double kMaxNoseOffsetRatio = 0.20;
constexpr double kMinSymmetryRatio = 0.62;
constexpr double kMinSharpnessVariance = 45.0;
std::string to_string(JNIEnv *env, jstring value) {
if (value == nullptr) return std::string();
const char *characters = env->GetStringUTFChars(value, nullptr);
if (characters == nullptr) return std::string();
std::string result(characters);
env->ReleaseStringUTFChars(value, characters);
return result;
}
unsigned char clamp_color(int value) {
return static_cast<unsigned char>(std::max(0, std::min(255, value)));
}
void nv21_to_bgr(const unsigned char *source,
int width,
int height,
int rotation,
bool mirror,
std::vector<unsigned char> &output,
int &output_width,
int &output_height) {
rotation = ((rotation % 360) + 360) % 360;
if (rotation != 90 && rotation != 180 && rotation != 270) rotation = 0;
output_width = (rotation == 90 || rotation == 270) ? height : width;
output_height = (rotation == 90 || rotation == 270) ? width : height;
output.assign(static_cast<size_t>(output_width) * output_height * 3, 0);
const int frame_size = width * height;
for (int sy = 0; sy < height; ++sy) {
const int uv_row = frame_size + (sy >> 1) * width;
for (int sx = 0; sx < width; ++sx) {
int y = static_cast<int>(source[sy * width + sx]) & 0xff;
int uv_index = uv_row + (sx & ~1);
int v = (static_cast<int>(source[uv_index]) & 0xff) - 128;
int u = (static_cast<int>(source[uv_index + 1]) & 0xff) - 128;
int c = std::max(0, y - 16);
int red = (298 * c + 409 * v + 128) >> 8;
int green = (298 * c - 100 * u - 208 * v + 128) >> 8;
int blue = (298 * c + 516 * u + 128) >> 8;
int rx = sx;
int ry = sy;
if (rotation == 90) {
rx = height - 1 - sy;
ry = sx;
} else if (rotation == 180) {
rx = width - 1 - sx;
ry = height - 1 - sy;
} else if (rotation == 270) {
rx = sy;
ry = width - 1 - sx;
}
if (mirror) rx = output_width - 1 - rx;
size_t index = (static_cast<size_t>(ry) * output_width + rx) * 3;
output[index] = clamp_color(blue);
output[index + 1] = clamp_color(green);
output[index + 2] = clamp_color(red);
}
}
}
bool face_center_is_in_capture_area(const SeetaRect &face, int image_width, int image_height) {
const double center_x = face.x + face.width * 0.5;
const double center_y = face.y + face.height * 0.5;
return center_x >= image_width * kCenterLeftRatio
&& center_x <= image_width * kCenterRightRatio
&& center_y >= image_height * kCenterTopRatio
&& center_y <= image_height * kCenterBottomRatio;
}
double point_distance(const SeetaPointF &first, const SeetaPointF &second) {
const double dx = first.x - second.x;
const double dy = first.y - second.y;
return std::sqrt(dx * dx + dy * dy);
}
double symmetry_ratio(double first, double second) {
const double largest = std::max(first, second);
return largest <= 0.0001 ? 0.0 : std::min(first, second) / largest;
}
bool landmarks_are_frontal(const std::vector<SeetaPointF> &points) {
if (points.size() < 5) return false;
const SeetaPointF &left_eye = points[0];
const SeetaPointF &right_eye = points[1];
const SeetaPointF &nose = points[2];
const SeetaPointF &left_mouth = points[3];
const SeetaPointF &right_mouth = points[4];
const double eye_width = std::fabs(right_eye.x - left_eye.x);
if (eye_width < 1.0) return false;
if (std::fabs(right_eye.y - left_eye.y) / eye_width > kMaxEyeTiltRatio) return false;
const double mouth_width = std::fabs(right_mouth.x - left_mouth.x);
if (mouth_width < 1.0
|| std::fabs(right_mouth.y - left_mouth.y) / mouth_width > kMaxMouthTiltRatio) {
return false;
}
const double eye_center_x = (left_eye.x + right_eye.x) * 0.5;
const double eye_center_y = (left_eye.y + right_eye.y) * 0.5;
const double mouth_center_x = (left_mouth.x + right_mouth.x) * 0.5;
const double mouth_center_y = (left_mouth.y + right_mouth.y) * 0.5;
if (std::fabs(nose.x - eye_center_x) / eye_width > kMaxNoseOffsetRatio) return false;
if (std::fabs(mouth_center_x - nose.x) / eye_width > kMaxNoseOffsetRatio) return false;
if (nose.y <= eye_center_y || mouth_center_y <= nose.y) return false;
if (symmetry_ratio(point_distance(nose, left_eye), point_distance(nose, right_eye))
< kMinSymmetryRatio) {
return false;
}
return symmetry_ratio(point_distance(nose, left_mouth), point_distance(nose, right_mouth))
>= kMinSymmetryRatio;
}
int gray_at(const unsigned char *bgr, int width, int x, int y) {
const size_t index = (static_cast<size_t>(y) * width + x) * 3;
return (29 * bgr[index] + 150 * bgr[index + 1] + 77 * bgr[index + 2]) >> 8;
}
double face_sharpness_variance(const unsigned char *bgr,
int image_width,
int image_height,
const SeetaRect &face) {
const int inset_x = std::max(2, face.width / 10);
const int inset_y = std::max(2, face.height / 10);
const int left = std::max(1, face.x + inset_x);
const int top = std::max(1, face.y + inset_y);
const int right = std::min(image_width - 2, face.x + face.width - inset_x);
const int bottom = std::min(image_height - 2, face.y + face.height - inset_y);
if (right <= left || bottom <= top) return 0.0;
double sum = 0.0;
double square_sum = 0.0;
long count = 0;
for (int y = top; y <= bottom; y += 2) {
for (int x = left; x <= right; x += 2) {
const int center = gray_at(bgr, image_width, x, y);
const int laplacian = 4 * center
- gray_at(bgr, image_width, x - 1, y)
- gray_at(bgr, image_width, x + 1, y)
- gray_at(bgr, image_width, x, y - 1)
- gray_at(bgr, image_width, x, y + 1);
sum += laplacian;
square_sum += static_cast<double>(laplacian) * laplacian;
++count;
}
}
if (count <= 0) return 0.0;
const double mean = sum / count;
return square_sum / count - mean * mean;
}
} // namespace
extern "C" JNIEXPORT jboolean JNICALL
Java_com_xingyuan_zhiling_SeetaFaceNative_initialize(
JNIEnv *env, jclass, jstring detector_model, jstring landmarker_model, jstring recognizer_model) {
std::lock_guard<std::mutex> lock(g_mutex);
try {
g_recognizer.reset();
g_landmarker.reset();
g_detector.reset();
std::string detector_path = to_string(env, detector_model);
std::string landmarker_path = to_string(env, landmarker_model);
std::string recognizer_path = to_string(env, recognizer_model);
if (detector_path.empty() || landmarker_path.empty() || recognizer_path.empty()) {
g_last_error = "模型路径不能为空";
return JNI_FALSE;
}
g_detector.reset(new seeta::FaceDetector(seeta::ModelSetting(detector_path, seeta::ModelSetting::CPU)));
g_landmarker.reset(new seeta::FaceLandmarker(seeta::ModelSetting(landmarker_path, seeta::ModelSetting::CPU)));
g_recognizer.reset(new seeta::FaceRecognizer(seeta::ModelSetting(recognizer_path, seeta::ModelSetting::CPU)));
g_detector->set(seeta::FaceDetector::PROPERTY_MIN_FACE_SIZE, 80);
g_detector->set(seeta::FaceDetector::PROPERTY_NUMBER_THREADS, 2);
g_recognizer->set(seeta::FaceRecognizer::PROPERTY_NUMBER_THREADS, 2);
g_last_error.clear();
return JNI_TRUE;
} catch (const std::exception &error) {
g_last_error = error.what();
} catch (...) {
g_last_error = "SeetaFace6 初始化发生未知错误";
}
g_recognizer.reset();
g_landmarker.reset();
g_detector.reset();
return JNI_FALSE;
}
extern "C" JNIEXPORT jfloatArray JNICALL
Java_com_xingyuan_zhiling_SeetaFaceNative_extractFeature(
JNIEnv *env, jclass, jbyteArray nv21, jint width, jint height, jint rotation, jboolean mirror) {
if (nv21 == nullptr || width <= 0 || height <= 0) return nullptr;
jsize length = env->GetArrayLength(nv21);
if (length < width * height * 3 / 2) return nullptr;
std::vector<unsigned char> source(static_cast<size_t>(length));
env->GetByteArrayRegion(nv21, 0, length, reinterpret_cast<jbyte *>(source.data()));
std::vector<unsigned char> bgr;
int image_width = 0;
int image_height = 0;
nv21_to_bgr(source.data(), width, height, rotation, mirror == JNI_TRUE, bgr, image_width, image_height);
std::lock_guard<std::mutex> lock(g_mutex);
if (!g_detector || !g_landmarker || !g_recognizer) {
g_last_error = "识别引擎未就绪";
return nullptr;
}
try {
SeetaImageData image = {image_width, image_height, 3, bgr.data()};
SeetaFaceInfoArray faces = g_detector->detect(image);
if (faces.size <= 0 || faces.data == nullptr) {
g_last_error = "画面中未检测到正面人脸";
return nullptr;
}
int selected = -1;
long largest_area = 0;
for (int i = 0; i < faces.size; ++i) {
if (!face_center_is_in_capture_area(faces.data[i].pos, image_width, image_height)) {
continue;
}
long area = static_cast<long>(faces.data[i].pos.width) * faces.data[i].pos.height;
if (area > largest_area) {
largest_area = area;
selected = i;
}
}
if (selected < 0) {
g_last_error = "请将人脸移至取景框中央";
return nullptr;
}
SeetaRect face = faces.data[selected].pos;
if (face.width < 80 || face.height < 80) {
g_last_error = "人脸距离较远,请靠近摄像头";
return nullptr;
}
int point_count = g_landmarker->number();
if (point_count < 5) {
g_last_error = "人脸关键点模型不可用";
return nullptr;
}
std::vector<SeetaPointF> points(static_cast<size_t>(point_count));
g_landmarker->mark(image, face, points.data());
if (!landmarks_are_frontal(points)) {
g_last_error = "请正对摄像头,侧脸不进行识别";
return nullptr;
}
if (face_sharpness_variance(bgr.data(), image_width, image_height, face)
< kMinSharpnessVariance) {
g_last_error = "画面模糊,请站稳并保持静止";
return nullptr;
}
int feature_size = g_recognizer->GetExtractFeatureSize();
if (feature_size <= 0) {
g_last_error = "人脸特征模型不可用";
return nullptr;
}
std::vector<float> feature(static_cast<size_t>(feature_size));
if (!g_recognizer->Extract(image, points.data(), feature.data())) {
g_last_error = "人脸特征提取失败,请保持正脸和充足光线";
return nullptr;
}
jfloatArray result = env->NewFloatArray(feature_size);
if (result == nullptr) return nullptr;
env->SetFloatArrayRegion(result, 0, feature_size, feature.data());
g_last_error.clear();
return result;
} catch (const std::exception &error) {
g_last_error = error.what();
} catch (...) {
g_last_error = "SeetaFace6 特征提取发生未知错误";
}
return nullptr;
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_xingyuan_zhiling_SeetaFaceNative_lastError(JNIEnv *env, jclass) {
std::lock_guard<std::mutex> lock(g_mutex);
return env->NewStringUTF(g_last_error.c_str());
}
extern "C" JNIEXPORT void JNICALL
Java_com_xingyuan_zhiling_SeetaFaceNative_release(JNIEnv *, jclass) {
std::lock_guard<std::mutex> lock(g_mutex);
g_recognizer.reset();
g_landmarker.reset();
g_detector.reset();
g_last_error.clear();
}
@@ -0,0 +1,23 @@
#pragma once
#include "Common/CStruct.h"
#ifdef __cplusplus
extern "C" {
#endif
struct SeetaFaceInfo
{
SeetaRect pos;
float score;
};
struct SeetaFaceInfoArray
{
struct SeetaFaceInfo *data;
int size;
};
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,97 @@
#ifndef INC_SEETA_C_STRUCT_H
#define INC_SEETA_C_STRUCT_H
#ifdef _MSC_VER
#ifdef SEETA_EXPORTS
#define SEETA_API __declspec(dllexport)
#else
#define SEETA_API __declspec(dllimport)
#endif
#else
#define SEETA_API __attribute__ ((visibility("default")))
#endif
#define SEETA_C_API extern "C" SEETA_API
#define INCLUDED_SEETA_CSTRUCT
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
struct SeetaImageData
{
int width;
int height;
int channels;
unsigned char *data;
};
struct SeetaPoint
{
int x;
int y;
};
struct SeetaPointF
{
double x;
double y;
};
struct SeetaSize
{
int width;
int height;
};
struct SeetaRect
{
int x;
int y;
int width;
int height;
};
struct SeetaRegion
{
int top;
int bottom;
int left;
int right;
};
enum SeetaDevice
{
SEETA_DEVICE_AUTO = 0,
SEETA_DEVICE_CPU = 1,
SEETA_DEVICE_GPU = 2,
};
struct SeetaModelSetting
{
enum SeetaDevice device;
int id; // when device is GPU, id means GPU id
const char **model; // model string terminate with nullptr
};
struct SeetaBuffer
{
void *buffer;
int64_t size;
};
struct SeetaModelBuffer
{
enum SeetaDevice device;
int id; // when device is GPU, id means GPU id
const SeetaBuffer *buffer; // input buffers, terminate with empty buffer(buffer=nullptr, size=0)
};
#ifdef __cplusplus
}
#endif
#endif // INC_SEETA_C_STRUCT_H
@@ -0,0 +1,703 @@
#ifndef INC_SEETA_STRUCT_H
#define INC_SEETA_STRUCT_H
#include "CStruct.h"
#include <memory>
#include <algorithm>
#include <vector>
#include <cstring>
#include <istream>
#include <string>
#define INCLUDED_SEETA_STRUCT
namespace seeta
{
class ImageData : public SeetaImageData
{
public:
using self = ImageData;
using supper = SeetaImageData;
using byte = unsigned char;
~ImageData() = default;
ImageData(const supper &image)
: ImageData(image.data, image.width, image.height, image.channels) {}
ImageData(int width, int height, int channels)
: supper({ width, height, channels, nullptr })
{
this->m_data.reset(new byte[this->count()], std::default_delete<byte[]>());
this->data = this->m_data.get();
}
ImageData() : ImageData(0, 0, 0) {}
ImageData(const byte *data, int width, int height, int channels)
: ImageData(width, height, channels)
{
this->copy_from(data);
}
ImageData(const self&) = default;
ImageData &operator=(const self &) = default;
ImageData &operator=(const supper &other)
{
this->operator=(self(other));
return *this;
}
ImageData(self &&other)
: supper({ other.width, other.height, other.channels, nullptr })
{
this->m_data = std::move(other.m_data);
this->data = this->m_data.get();
}
ImageData &operator=(self &&other)
{
this->width = other.width;
this->height = other.height;
this->channels = other.channels;
this->m_data = std::move(other.m_data);
this->data = this->m_data.get();
return *this;
}
void copy_from(const byte *data, int size = -1)
{
int copy_size = this->count();
copy_size = size < 0 ? copy_size : std::min<int>(copy_size, size);
copy(this->data, data, copy_size);
}
void copy_to(byte *data, int size = -1) const
{
int copy_size = this->count();
copy_size = size < 0 ? copy_size : std::min<int>(copy_size, size);
copy(data, this->data, copy_size);
}
static void copy(byte *dst, const byte *src, size_t size)
{
std::memcpy(dst, src, size);
}
int count() const { return this->width * this->height * this->channels; }
ImageData clone() const
{
return ImageData(this->data, this->width, this->height, this->channels);
}
private:
std::shared_ptr<byte> m_data;
};
class Point : public SeetaPoint
{
public:
using self = Point;
using supper = SeetaPoint;
Point(const supper &other) : supper(other) {}
Point(int x, int y) : supper({ x, y }) {}
Point() : Point(0, 0) {}
};
class PointF : public SeetaPointF
{
public:
using self = PointF;
using supper = SeetaPointF;
PointF(const supper &other) : supper(other) {}
PointF(double x, double y) : supper({ x, y }) {}
PointF() : PointF(0, 0) {}
};
class Size : public SeetaSize
{
public:
using self = Size;
using supper = SeetaSize;
Size(const supper &other) : supper(other) {}
Size(int width, int height) : supper({ width, height }) {}
Size() : Size(0, 0) {}
};
class Rect : public SeetaRect
{
public:
using self = Rect;
using supper = SeetaRect;
Rect(const supper &other) : supper(other) {}
Rect(int x, int y, int width, int height) : supper({ x, y, width, height }) {}
Rect() : Rect(0, 0, 0, 0) {}
Rect(int x, int y, const Size &size) : supper({ x, y, size.width, size.height }) {}
Rect(const Point &top_left, int width, int height) : supper({ top_left.x, top_left.y, width, height }) {}
Rect(const Point &top_left, const Size &size) : supper({ top_left.x, top_left.y, size.width, size.height }) {}
Rect(const Point &top_left, const Point &bottom_right) : supper({ top_left.x, top_left.y, bottom_right.x - top_left.x, bottom_right.y - top_left.y }) {}
operator Point() const { return{ this->x, this->y }; }
operator Size() const { return{ this->width, this->height }; }
};
class Region : public SeetaRegion
{
public:
using self = Region;
using supper = SeetaRegion;
Region(const supper &other) : supper(other) {}
Region(int top, int bottom, int left, int right) : supper({ top, bottom, left, right }) {}
Region() : Region(0, 0, 0, 0) {}
Region(const Rect &rect) : Region(rect.y, rect.y + rect.height, rect.x, rect.x + rect.width) {}
operator Rect() const { return{ left, top, right - left, bottom - top }; }
};
class ModelSetting : public SeetaModelSetting
{
public:
using self = ModelSetting;
using supper = SeetaModelSetting;
enum Device
{
AUTO,
CPU,
GPU
};
~ModelSetting() = default;
ModelSetting()
: supper({ SEETA_DEVICE_AUTO, 0, nullptr })
{
this->update();
}
ModelSetting(const supper &other)
: supper({ other.device, other.id, nullptr })
{
if (other.model) {
int i = 0;
while (other.model[i])
{
m_model_string.emplace_back(other.model[i]);
++i;
}
}
this->update();
}
ModelSetting(const self &other)
: supper({ other.device, other.id, nullptr })
{
this->m_model_string = other.m_model_string;
this->update();
}
ModelSetting &operator=(const supper &other)
{
this->operator=(self(other));
return *this;
}
ModelSetting &operator=(const self &other)
{
this->device = other.device;
this->id = other.id;
this->m_model_string = other.m_model_string;
this->update();
return *this;
}
ModelSetting(self &&other)
: supper({ other.device, other.id, nullptr })
{
this->m_model_string = std::move(other.m_model_string);
this->update();
}
ModelSetting &operator=(self &&other)
{
this->device = other.device;
this->id = other.id;
this->m_model_string = std::move(other.m_model_string);
this->update();
return *this;
}
ModelSetting(const std::string &model, SeetaDevice device, int id)
: supper({ device, id, nullptr })
{
this->append(model);
}
ModelSetting(const std::string &model, SeetaDevice device) : self(model, device, 0) {}
ModelSetting(const std::string &model, Device device, int id) : self(model, SeetaDevice(device), id) {}
ModelSetting(const std::string &model, Device device) : self(model, SeetaDevice(device)) {}
ModelSetting(const std::string &model) : self(model, SEETA_DEVICE_AUTO) {}
ModelSetting(const std::vector<std::string> &model, SeetaDevice device, int id)
: supper({ device, id, nullptr })
{
this->append(model);
}
ModelSetting(const std::vector<std::string> &model, SeetaDevice device) : self(model, device, 0) {}
ModelSetting(const std::vector<std::string> &model, Device device, int id) : self(model, SeetaDevice(device), id) {}
ModelSetting(const std::vector<std::string> &model, Device device) : self(model, SeetaDevice(device)) {}
ModelSetting(const std::vector<std::string> &model) : self(model, SEETA_DEVICE_AUTO) {}
ModelSetting(SeetaDevice device, int id)
: supper({ device, id, nullptr })
{
this->update();
}
ModelSetting(SeetaDevice device) : self(device, 0) {}
ModelSetting(Device device, int id) : self(SeetaDevice(device), id) {}
ModelSetting(Device device) : self(SeetaDevice(device)) {}
Device get_device() const { return Device(this->device); }
int get_id() const { return this->id; }
Device set_device(Device device)
{
return set_device(SeetaDevice(device));
}
Device set_device(SeetaDevice device)
{
auto old = this->device;
this->device = device;
return Device(old);
}
int set_id(int id)
{
const auto old = this->id;
this->id = id;
return old;
}
void clear()
{
this->m_model_string.clear();
this->update();
}
void append(const std::string &model)
{
this->m_model_string.push_back(model);
this->update();
}
void append(const std::vector<std::string> &model)
{
this->m_model_string.insert(this->m_model_string.end(), model.begin(), model.end());
this->update();
}
const std::vector<std::string> &get_model() const
{
return this->m_model_string;
}
const std::string &get_model(size_t i) const
{
return this->m_model_string[i];
}
size_t count() const
{
return this->m_model_string.size();
}
private:
std::vector<const char *> m_model;
std::vector<std::string> m_model_string;
/**
* \brief build buffer::model
*/
void update()
{
m_model.clear();
m_model.reserve(m_model_string.size() + 1);
for (auto &model_string : m_model_string)
{
m_model.push_back(model_string.c_str());
}
m_model.push_back(nullptr);
this->model = m_model.data();
}
};
class Buffer : public SeetaBuffer
{
public:
using self = Buffer;
using supper = SeetaBuffer;
using byte = unsigned char;
~Buffer() = default;
Buffer(const supper &other) : Buffer(other.buffer, other.size) {}
Buffer(const self &) = default;
Buffer &operator=(const self &) = default;
Buffer &operator=(const supper &other)
{
this->operator=(self(other));
return *this;
}
Buffer(self &&other)
: supper({ other.buffer, other.size }), m_buffer(std::move(other.m_buffer)), m_size(other.m_size) {}
Buffer &operator=(self &&other)
{
this->buffer = other.buffer;
this->size = other.size;
this->m_buffer = std::move(other.m_buffer);
this->m_size = other.m_size;
return *this;
}
/**
* \brief contruct cpp-style-buffer from c-style-buffer, optional borrow or copy
* \param other c-style-buffer
* \param borrow if borrow or copy buffer
*/
Buffer(const supper &other, bool borrow)
: supper({ nullptr, 0 })
{
if (borrow) this->borrow(other.buffer, other.size);
else this->rebind(other.buffer, other.size);
}
Buffer(const void *buffer, int64_t size)
: supper({ nullptr, 0 })
{
this->m_size = size;
if (this->m_size)
{
this->m_buffer.reset(new byte[size_t(this->m_size)], std::default_delete<byte[]>());
}
this->buffer = this->m_buffer.get();
this->size = this->m_size;
if (buffer != nullptr)
{
this->copy_from(buffer, size);
}
}
explicit Buffer(int64_t size) : Buffer(nullptr, size) {}
Buffer() : Buffer(nullptr, 0) {}
Buffer(std::istream &in, int64_t size = -1)
: supper({ nullptr, 0 })
{
if (size < 0)
{
const auto cur = in.tellg();
in.seekg(0, std::ios::end);
const auto end = in.tellg();
size = int64_t(end - cur);
in.seekg(-size, std::ios::end);
}
this->m_size = size;
this->m_buffer.reset(new byte[size_t(this->m_size)], std::default_delete<byte[]>());
this->buffer = this->m_buffer.get();
this->size = this->m_size;
in.read(reinterpret_cast<char*>(this->buffer), this->size);
}
void copy_from(const void *data, int64_t size = -1)
{
auto copy_size = this->m_size;
copy_size = size < 0 ? copy_size : std::min<int64_t>(copy_size, size);
copy(this->buffer, data, size_t(copy_size));
}
void copy_to(byte *data, int64_t size = -1) const
{
auto copy_size = this->m_size;
copy_size = size < 0 ? copy_size : std::min<int64_t>(copy_size, size);
copy(data, this->buffer, size_t(copy_size));
}
static void copy(void *dst, const void *src, size_t size)
{
if (dst == nullptr || src == nullptr) return;
std::memcpy(dst, src, size);
}
Buffer clone() const
{
return self(this->buffer, this->size);
}
void rebind(const void *buffer, int64_t size)
{
if (size < 0) size = 0;
if (size > this->m_size)
{
this->m_buffer.reset(new byte[size_t(size)], std::default_delete<byte[]>());
}
this->m_size = size;
this->buffer = this->m_buffer.get();
this->size = this->m_size;
this->copy_from(buffer, size);
}
/**
* \brief borrow c-style buffer
* \param buffer pointert to buffer
* \param size size of buffer
*/
void borrow(void *buffer, int64_t size)
{
this->m_size = 0;
this->m_buffer.reset();
this->buffer = buffer;
this->size = size;
}
private:
std::shared_ptr<byte> m_buffer;
int64_t m_size = 0;
};
class ModelBuffer : SeetaModelBuffer
{
public:
using self = ModelBuffer;
using supper = SeetaModelBuffer;
enum Device
{
AUTO = SEETA_DEVICE_AUTO,
CPU = SEETA_DEVICE_CPU,
GPU = SEETA_DEVICE_GPU,
};
~ModelBuffer() = default;
ModelBuffer()
: supper({ SEETA_DEVICE_AUTO, 0, nullptr })
{
this->update();
}
ModelBuffer(const supper &other)
: supper({ other.device, other.id, nullptr })
{
if (other.buffer) {
int i = 0;
while (other.buffer[i].buffer && other.buffer[i].size)
{
m_model_buffer.emplace_back(other.buffer[i]);
++i;
}
}
this->update();
}
ModelBuffer(const self &other)
: supper({ other.device, other.id, nullptr })
{
this->m_model_buffer = other.m_model_buffer;
this->update();
}
ModelBuffer &operator=(const supper &other)
{
this->operator=(self(other));
return *this;
}
ModelBuffer &operator=(const self &other)
{
this->device = other.device;
this->id = other.id;
this->m_model_buffer = other.m_model_buffer;
this->update();
return *this;
}
ModelBuffer(self &&other)
: supper({ other.device, other.id, nullptr })
{
this->m_model_buffer = std::move(other.m_model_buffer);
this->update();
}
ModelBuffer &operator=(self &&other)
{
this->device = other.device;
this->id = other.id;
this->m_model_buffer = std::move(other.m_model_buffer);
this->update();
return *this;
}
ModelBuffer(const seeta::Buffer &buffer, SeetaDevice device, int id)
: supper({ device, id, nullptr })
{
this->append(buffer);
}
ModelBuffer(const seeta::Buffer &buffer, SeetaDevice device) : self(buffer, device, 0) {}
ModelBuffer(const seeta::Buffer &buffer, Device device, int id) : self(buffer, SeetaDevice(device), id) {}
ModelBuffer(const seeta::Buffer &buffer, Device device) : self(buffer, SeetaDevice(device)) {}
ModelBuffer(const seeta::Buffer &buffer) : self(buffer, SEETA_DEVICE_AUTO) {}
ModelBuffer(const std::vector<seeta::Buffer> &buffer, SeetaDevice device, int id)
: supper({ device, id, nullptr })
{
this->append(buffer);
}
ModelBuffer(const std::vector<seeta::Buffer> &buffer, SeetaDevice device) : self(buffer, device, 0) {}
ModelBuffer(const std::vector<seeta::Buffer> &buffer, Device device, int id) : self(buffer, SeetaDevice(device), id) {}
ModelBuffer(const std::vector<seeta::Buffer> &buffer, Device device) : self(buffer, SeetaDevice(device)) {}
ModelBuffer(const std::vector<seeta::Buffer> &buffer) : self(buffer, SEETA_DEVICE_AUTO) {}
ModelBuffer(SeetaDevice device, int id)
: supper({ device, id, nullptr })
{
this->update();
}
ModelBuffer(SeetaDevice device) : self(device, 0) {}
ModelBuffer(Device device, int id) : self(SeetaDevice(device), id) {}
ModelBuffer(Device device) : self(SeetaDevice(device)) {}
Device get_device() const { return Device(this->device); }
int get_id() const { return this->id; }
Device set_device(Device device)
{
return set_device(SeetaDevice(device));
}
Device set_device(SeetaDevice device)
{
auto old = this->device;
this->device = device;
return Device(old);
}
int set_id(int id)
{
const auto old = this->id;
this->id = id;
return old;
}
void clear()
{
this->m_model_buffer.clear();
this->update();
}
void append(const seeta::Buffer &buffer)
{
this->m_model_buffer.push_back(buffer);
this->update();
}
void append(const std::vector<seeta::Buffer> &model)
{
this->m_model_buffer.insert(this->m_model_buffer.end(), model.begin(), model.end());
this->update();
}
const std::vector<seeta::Buffer> &get_buffer() const
{
return this->m_model_buffer;
}
const seeta::Buffer &get_buffer(size_t i) const
{
return this->m_model_buffer[i];
}
size_t count() const
{
return this->m_model_buffer.size();
}
private:
std::vector<SeetaBuffer> m_buffer;
std::vector<seeta::Buffer> m_model_buffer;
/**
* \brief build supper::buffer
*/
void update()
{
this->m_buffer.clear();
this->m_buffer.reserve(m_model_buffer.size() + 1);
for (auto &model_buffer : m_model_buffer)
{
this->m_buffer.push_back(model_buffer);
}
this->m_buffer.push_back({ nullptr, 0 }); // terminate with empty buffer
this->buffer = this->m_buffer.data();
}
};
}
#endif
@@ -0,0 +1,54 @@
//
// Created by kier on 19-4-24.
//
#ifndef INC_SEETA_FACEDETECTOR_H
#define INC_SEETA_FACEDETECTOR_H
#include "seeta/Common/Struct.h"
#include "seeta/CFaceInfo.h"
#include "seeta/SeetaFaceDetectorConfig.h"
namespace seeta {
namespace SEETA_FACE_DETECTOR_NAMESPACE_VERSION {
class FaceDetector {
public:
using self = FaceDetector;
enum Property {
PROPERTY_MIN_FACE_SIZE,
PROPERTY_THRESHOLD,
PROPERTY_MAX_IMAGE_WIDTH,
PROPERTY_MAX_IMAGE_HEIGHT,
PROPERTY_NUMBER_THREADS,
PROPERTY_ARM_CPU_MODE = 0x101,
};
SEETA_API explicit FaceDetector(const SeetaModelSetting &setting);
SEETA_API ~FaceDetector();
SEETA_API explicit FaceDetector(const self *other);
SEETA_API SeetaFaceInfoArray detect(const SeetaImageData &image) const;
SEETA_API void set(Property property, double value);
SEETA_API double get(Property property) const;
private:
FaceDetector(const FaceDetector &) = delete;
const FaceDetector &operator=(const FaceDetector &) = delete;
private:
class Implement;
Implement *m_impl;
};
}
using namespace SEETA_FACE_DETECTOR_NAMESPACE_VERSION;
}
#endif //INC_SEETA_FACEDETECTOR_H
@@ -0,0 +1,64 @@
//
// Created by kier on 19-7-16.
//
#ifndef SEETA_FACELANDMARKER_FACELANDMARKER_H
#define SEETA_FACELANDMARKER_FACELANDMARKER_H
#include "Common/Struct.h"
#include "seeta/SeetaFaceLandmarkerConfig.h"
namespace seeta {
namespace SEETA_FACE_LANDMARKER_NAMESPACE_VERSION {
class FaceLandmarker {
public:
using self = FaceLandmarker;
SEETA_API explicit FaceLandmarker(const SeetaModelSetting &setting);
SEETA_API ~FaceLandmarker();
SEETA_API FaceLandmarker(const self *other);
SEETA_API int number() const;
SEETA_API void mark(const SeetaImageData &image, const SeetaRect &face, SeetaPointF *points) const;
SEETA_API void mark(const SeetaImageData &image, const SeetaRect &face, SeetaPointF *points, int32_t *mask) const;
std::vector<SeetaPointF> mark(const SeetaImageData &image, const SeetaRect &face) const {
std::vector<SeetaPointF> points(this->number());
mark(image, face, points.data());
return points;
}
class PointWithMask {
public:
SeetaPointF point;
bool mask;
};
std::vector<PointWithMask> mark_v2(const SeetaImageData &image, const SeetaRect &face) const {
std::vector<SeetaPointF> points(this->number());
std::vector<int32_t> masks(this->number());
mark(image, face, points.data(), masks.data());
std::vector<PointWithMask> point_with_masks(this->number());
for (int i = 0; i < this->number(); ++i) {
point_with_masks[i].point = points[i];
point_with_masks[i].mask = masks[i];
}
return point_with_masks;
}
private:
FaceLandmarker(const FaceLandmarker &) = delete;
const FaceLandmarker &operator=(const FaceLandmarker&) = delete;
private:
class Implement;
Implement *m_impl;
};
}
using namespace SEETA_FACE_LANDMARKER_NAMESPACE_VERSION;
}
#endif //SEETA_FACELANDMARKER_FACELANDMARKER_H
@@ -0,0 +1,81 @@
//
// Created by kier on 19-4-24.
//
#ifndef SEETA_FACERECOGNIZER_FACERECOGNIZER_H
#define SEETA_FACERECOGNIZER_FACERECOGNIZER_H
#include "Common/Struct.h"
#include "seeta/SeetaFaceRecognizerConfig.h"
// #define SEETA_FACE_RECOGNIZER_MAJOR_VERSION 6
// #define SEETA_FACE_RECOGNIZER_MINOR_VERSION 0
// #define SEETA_FACE_RECOGNIZER_SINOR_VERSION 0
namespace seeta {
namespace SEETA_FACE_RECOGNIZE_NAMESPACE_VERSION {
class FaceRecognizer {
public:
using self = FaceRecognizer;
enum Property {
PROPERTY_NUMBER_THREADS = 4,
PROPERTY_ARM_CPU_MODE = 5
};
SEETA_API explicit FaceRecognizer(const SeetaModelSetting &setting);
SEETA_API ~FaceRecognizer();
SEETA_API FaceRecognizer(const self *other);
SEETA_API static int GetCropFaceWidth();
SEETA_API static int GetCropFaceHeight();
SEETA_API static int GetCropFaceChannels();
SEETA_API static bool CropFace(const SeetaImageData &image, const SeetaPointF *points, SeetaImageData &face);
SEETA_API int GetExtractFeatureSize() const;
SEETA_API bool ExtractCroppedFace(const SeetaImageData &image, float *features) const;
SEETA_API bool Extract(const SeetaImageData &image, const SeetaPointF *points, float *features) const;
SEETA_API float CalculateSimilarity(const float *features1, const float *features2) const;
static seeta::ImageData CropFace(const SeetaImageData &image, const SeetaPointF *points) {
seeta::ImageData face(GetCropFaceWidth(), GetCropFaceHeight(), GetCropFaceChannels());
CropFace(image, points, face);
return face;
}
SEETA_API int GetCropFaceWidthV2() const;
SEETA_API int GetCropFaceHeightV2() const;
SEETA_API int GetCropFaceChannelsV2() const;
SEETA_API bool CropFaceV2(const SeetaImageData &image, const SeetaPointF *points, SeetaImageData &face);
seeta::ImageData CropFaceV2(const SeetaImageData &image, const SeetaPointF *points) {
seeta::ImageData face(GetCropFaceWidthV2(), GetCropFaceHeightV2(), GetCropFaceChannelsV2());
CropFaceV2(image, points, face);
return face;
}
SEETA_API void set(Property property, double value);
SEETA_API double get(Property property) const;
private:
FaceRecognizer(const FaceRecognizer &) = delete;
const FaceRecognizer &operator=(const FaceRecognizer&) = delete;
private:
class Implement;
Implement *m_impl;
};
}
using namespace SEETA_FACE_RECOGNIZE_NAMESPACE_VERSION;
}
#endif //SEETA_FACERECOGNIZER_FACERECOGNIZER_H
@@ -0,0 +1,12 @@
#ifndef _INC_SEETA_FACE_DETECTOR_V6_
#define _INC_SEETA_FACE_DETECTOR_V6_
#define SEETA_FACE_DETECTOR_MAJOR_VERSION 6
#define SEETA_FACE_DETECTOR_MINOR_VERSION 0
#define SEETA_FACE_DETECTOR_SINOR_VERSION 0
#define SEETA_FACE_DETECTOR_LIBRARY_NAME "SeetaFaceDetector"
#define SEETA_FACE_DETECTOR_NAMESPACE_VERSION v6
#endif // _INC_SEETA_FACE_DETECTOR_V6_
@@ -0,0 +1,9 @@
#pragma once
#define SEETA_FACE_LANDMARKER_MAJOR_VERSION 6
#define SEETA_FACE_LANDMARKER_MINOR_VERSION 0
#define SEETA_FACE_LANDMARKER_SINOR_VERSION 0
#define SEETA_FACE_LANDMARKER_LIBRARY_NAME "SeetaFaceLandmarker"
#define SEETA_FACE_LANDMARKER_NAMESPACE_VERSION v6
@@ -0,0 +1,9 @@
#pragma once
#define SEETA_FACE_RECOGNIZER_MAJOR_VERSION 6
#define SEETA_FACE_RECOGNIZER_MINOR_VERSION 1
#define SEETA_FACE_RECOGNIZER_SINOR_VERSION 0
#define SEETA_FACE_RECOGNIZE_LIBRARY_NAME "SeetaFaceRecognizer"
#define SEETA_FACE_RECOGNIZE_NAMESPACE_VERSION v6
@@ -0,0 +1,67 @@
/*
* Copyright 2009 Cedric Priscal
*
* Licensed under the Apache License, Version 2.0.
*/
package android_serialport_api;
import android.util.Log;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class SerialPort {
private static final String TAG = "SerialPort";
/*
* Do not remove or rename this field: it is used by native close().
*/
private FileDescriptor mFd;
private FileInputStream mFileInputStream;
private FileOutputStream mFileOutputStream;
public SerialPort(File device, int baudrate, int flags) throws SecurityException, IOException {
if (!device.canRead() || !device.canWrite()) {
try {
Process su = Runtime.getRuntime().exec("/system/bin/su");
String cmd = "chmod 666 " + device.getAbsolutePath() + "\nexit\n";
su.getOutputStream().write(cmd.getBytes());
if ((su.waitFor() != 0) || !device.canRead() || !device.canWrite()) {
throw new SecurityException();
}
} catch (Exception error) {
throw new SecurityException(error);
}
}
mFd = open(device.getAbsolutePath(), baudrate, flags);
if (mFd == null) {
Log.e(TAG, "native open returns null");
throw new IOException("native open returns null");
}
mFileInputStream = new FileInputStream(mFd);
mFileOutputStream = new FileOutputStream(mFd);
}
public InputStream getInputStream() {
return mFileInputStream;
}
public OutputStream getOutputStream() {
return mFileOutputStream;
}
private native static FileDescriptor open(String path, int baudrate, int flags);
public native void close();
static {
System.loadLibrary("serial_port");
}
}
@@ -0,0 +1,51 @@
package com.xingyuan.zhiling;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
/**
* 开机自启动接收器。
*
* 监听 android.intent.action.BOOT_COMPLETED 广播,
* 设备开机完成后自动拉起 MainActivity。
*
* 注意:该广播只有在以下条件同时满足时才会被接收:
* 1. 应用已安装(非刚安装后首次,需经历过一次重启或手动启动过 app);
* 2. 拥有 RECEIVE_BOOT_COMPLETED 权限。
*
* 对于嵌入式设备,建议同时将 MainActivity 设为默认桌面(HOME),
* 这样即使 BootReceiver 因系统策略延迟,也能在用户按 Home 键时回到 app。
*/
public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null) {
return;
}
String action = intent.getAction();
if (action == null) {
return;
}
// 开机完成
if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
launchMainActivity(context);
return;
}
// 部分定制系统在替换系统包或更新后会发送这些广播
if ("android.intent.action.QUICKBOOT_POWERON".equals(action)
|| "com.android.intent.action.QUICKBOOT_POWERON".equals(action)) {
launchMainActivity(context);
}
}
private void launchMainActivity(Context context) {
Intent launchIntent = new Intent(context, MainActivity.class);
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(launchIntent);
}
}
@@ -0,0 +1,202 @@
package com.xingyuan.zhiling;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
final class CrashRecovery {
private static final String PREFS = "xingyuan_crash_recovery";
private static final String KEY_LAST_CRASH_AT = "last_crash_at";
private static final String KEY_CRASH_COUNT = "crash_count";
private static final String KEY_NOTICE = "notice";
private static final String KEY_STARTUP_CRASH = "startup_crash";
private static final String KEY_RETRY_GRANTED = "retry_granted";
private static final long CRASH_WINDOW_MS = 60_000L;
private static final int MAX_RESTARTS_IN_WINDOW = 3;
private static final long RESTART_DELAY_MS = 1_500L;
private CrashRecovery() {
}
static void recordAndSchedule(Context context, Throwable error) {
Context app = context.getApplicationContext();
SharedPreferences preferences = app.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
long now = System.currentTimeMillis();
long lastCrashAt = preferences.getLong(KEY_LAST_CRASH_AT, 0L);
int count = now - lastCrashAt <= CRASH_WINDOW_MS
? preferences.getInt(KEY_CRASH_COUNT, 0) + 1
: 1;
String summary = summarize(error);
preferences.edit()
.putLong(KEY_LAST_CRASH_AT, now)
.putInt(KEY_CRASH_COUNT, count)
.putString(KEY_NOTICE, summary)
.putBoolean(KEY_STARTUP_CRASH, isStartupCrash(error))
.putBoolean(KEY_RETRY_GRANTED, false)
.commit();
writeCrashLog(app, now, error);
if (count <= MAX_RESTARTS_IN_WINDOW) {
scheduleRestart(app);
} else {
preferences.edit().putString(KEY_NOTICE,
summary + ";一分钟内已连续异常 " + count + " 次,已停止自动重启以避免循环崩溃").commit();
}
}
static void recordRendererCrashAndRestart(Context context, boolean didCrash) {
RuntimeException error = new RuntimeException(
didCrash ? "WebView 渲染进程崩溃" : "WebView 渲染进程被系统回收");
recordAndSchedule(context, error);
}
static String consumeRecoveryNotice(Context context) {
SharedPreferences preferences = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
String notice = preferences.getString(KEY_NOTICE, "");
if (notice == null || notice.length() == 0) {
return "";
}
preferences.edit().remove(KEY_NOTICE).apply();
return "应用已从异常退出中自动恢复:" + notice;
}
static boolean shouldEnterStartupRecovery(Context context) {
SharedPreferences preferences = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
if (preferences.getBoolean(KEY_RETRY_GRANTED, false)) {
preferences.edit().putBoolean(KEY_RETRY_GRANTED, false).commit();
return false;
}
boolean startupCrash = preferences.getBoolean(KEY_STARTUP_CRASH, false);
long lastCrashAt = preferences.getLong(KEY_LAST_CRASH_AT, 0L);
int count = preferences.getInt(KEY_CRASH_COUNT, 0);
boolean repeatedRecentCrash = count >= 2
&& System.currentTimeMillis() - lastCrashAt <= CRASH_WINDOW_MS;
return startupCrash || repeatedRecentCrash;
}
static boolean canAutoRetryStartup(Context context) {
return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.getInt(KEY_CRASH_COUNT, 0) <= MAX_RESTARTS_IN_WINDOW;
}
static String peekRecoveryNotice(Context context) {
String notice = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.getString(KEY_NOTICE, "");
return notice == null || notice.length() == 0 ? "未知异常" : notice;
}
static void clearStartupRecoveryForRetry(Context context) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.edit()
.putBoolean(KEY_STARTUP_CRASH, false)
.putBoolean(KEY_RETRY_GRANTED, true)
.commit();
}
static void markLaunchSuccessful(Context context) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.edit()
.putLong(KEY_LAST_CRASH_AT, 0L)
.putInt(KEY_CRASH_COUNT, 0)
.putBoolean(KEY_STARTUP_CRASH, false)
.putBoolean(KEY_RETRY_GRANTED, false)
.apply();
}
private static void scheduleRestart(Context context) {
Intent intent = new Intent(context, MainActivity.class);
intent.setAction("com.xingyuan.zhiling.action.CRASH_RECOVERY");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_SINGLE_TOP);
int flags = PendingIntent.FLAG_CANCEL_CURRENT;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
flags |= PendingIntent.FLAG_IMMUTABLE;
}
PendingIntent pendingIntent = PendingIntent.getActivity(context, 7311, intent, flags);
AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (alarm == null) {
return;
}
long triggerAt = System.currentTimeMillis() + RESTART_DELAY_MS;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
alarm.setExact(AlarmManager.RTC_WAKEUP, triggerAt, pendingIntent);
} else {
alarm.set(AlarmManager.RTC_WAKEUP, triggerAt, pendingIntent);
}
}
private static String summarize(Throwable error) {
if (error == null) {
return "未知异常";
}
String type = error.getClass().getSimpleName();
String message = error.getMessage();
if (message == null || message.trim().length() == 0) {
return type;
}
String normalized = message.replace('\n', ' ').replace('\r', ' ').trim();
if (normalized.length() > 180) {
normalized = normalized.substring(0, 180);
}
return type + " - " + normalized;
}
private static boolean isStartupCrash(Throwable error) {
Throwable current = error;
while (current != null) {
StackTraceElement[] stack = current.getStackTrace();
if (stack != null) {
for (StackTraceElement element : stack) {
if (MainActivity.class.getName().equals(element.getClassName())
&& "onCreate".equals(element.getMethodName())) {
return true;
}
}
}
current = current.getCause();
}
return false;
}
private static void writeCrashLog(Context context, long timestamp, Throwable error) {
File directory = new File(context.getFilesDir(), "crash_logs");
if (!directory.exists() && !directory.mkdirs()) {
return;
}
File target = new File(directory, "last-crash.log");
FileWriter writer = null;
try {
writer = new FileWriter(target, false);
writer.write(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.CHINA)
.format(new Date(timestamp)));
writer.write('\n');
StringWriter stack = new StringWriter();
PrintWriter printer = new PrintWriter(stack);
if (error != null) {
error.printStackTrace(printer);
}
printer.flush();
writer.write(stack.toString());
writer.flush();
} catch (Throwable ignored) {
} finally {
if (writer != null) {
try {
writer.close();
} catch (Exception ignored) {
}
}
}
}
}
@@ -0,0 +1,256 @@
package com.xingyuan.zhiling;
import android.content.Context;
import android.util.AtomicFile;
import android.util.Base64;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import org.json.JSONArray;
import org.json.JSONObject;
final class FaceFeatureStore {
static final class Match {
final String faceId;
final String employeeNo;
final String name;
final float similarity;
Match(String faceId, String employeeNo, String name, float similarity) {
this.faceId = faceId;
this.employeeNo = employeeNo;
this.name = name;
this.similarity = similarity;
}
}
private static final class Record {
final String faceId;
final String employeeNo;
final String name;
final float[] feature;
Record(String faceId, String employeeNo, String name, float[] feature) {
this.faceId = faceId;
this.employeeNo = employeeNo;
this.name = name;
this.feature = feature;
}
}
private static final Charset UTF8 = Charset.forName("UTF-8");
private final AtomicFile atomicStore;
private final List<Record> records = new ArrayList<Record>();
FaceFeatureStore(Context context) {
atomicStore = new AtomicFile(new File(context.getFilesDir(), "xingyuan_face_features.json"));
load();
}
synchronized int size() {
return records.size();
}
synchronized JSONArray listPeople() {
JSONArray people = new JSONArray();
for (Record record : records) {
try {
JSONObject person = new JSONObject();
person.put("faceId", record.faceId);
person.put("employeeNo", record.employeeNo);
person.put("name", record.name);
person.put("registered", true);
people.put(person);
} catch (Exception ignored) {
}
}
return people;
}
synchronized boolean remove(String faceId) throws Exception {
String normalizedId = safe(faceId);
List<Record> previous = new ArrayList<Record>(records);
boolean removed = false;
for (int i = records.size() - 1; i >= 0; i -= 1) {
if (normalizedId.equals(records.get(i).faceId)) {
records.remove(i);
removed = true;
}
}
if (removed) {
try {
save();
} catch (Exception error) {
// 磁盘写入失败时恢复内存快照,避免运行中的名单与磁盘人脸库不一致。
records.clear();
records.addAll(previous);
throw error;
}
}
return removed;
}
synchronized void put(String faceId, String employeeNo, String name, float[] feature) throws Exception {
if (faceId == null || faceId.trim().length() == 0 || feature == null || feature.length == 0) {
throw new IllegalArgumentException("人脸编号和特征不能为空");
}
String normalizedId = faceId.trim();
List<Record> previous = new ArrayList<Record>(records);
for (int i = records.size() - 1; i >= 0; i -= 1) {
if (normalizedId.equals(records.get(i).faceId)) {
records.remove(i);
}
}
records.add(new Record(normalizedId, safe(employeeNo), safe(name), feature.clone()));
try {
save();
} catch (Exception error) {
records.clear();
records.addAll(previous);
throw error;
}
}
synchronized Match findBest(float[] probe, float threshold) {
if (probe == null || probe.length == 0) {
return null;
}
Record best = null;
float bestScore = -1f;
for (Record record : records) {
if (record.feature.length != probe.length) {
continue;
}
float score = cosine(record.feature, probe);
if (score > bestScore) {
bestScore = score;
best = record;
}
}
if (best == null || bestScore < threshold) {
return null;
}
return new Match(best.faceId, best.employeeNo, best.name, bestScore);
}
private void load() {
synchronized (this) {
records.clear();
if (!atomicStore.getBaseFile().exists()) {
return;
}
StringBuilder text = new StringBuilder();
try {
// AtomicFile 会在上次写入中断时自动回退到 .bak,避免人员库变空。
BufferedReader reader = new BufferedReader(
new InputStreamReader(atomicStore.openRead(), UTF8));
try {
String line;
while ((line = reader.readLine()) != null) {
text.append(line);
}
} finally {
reader.close();
}
JSONArray array = new JSONArray(text.toString());
for (int i = 0; i < array.length(); i += 1) {
JSONObject item = array.optJSONObject(i);
if (item == null) {
continue;
}
float[] feature = decode(item.optString("feature", ""));
if (feature.length == 0) {
continue;
}
records.add(new Record(
item.optString("faceId", ""),
item.optString("employeeNo", ""),
item.optString("name", ""),
feature));
}
} catch (Exception ignored) {
records.clear();
}
}
}
private void save() throws Exception {
JSONArray array = new JSONArray();
for (Record record : records) {
JSONObject item = new JSONObject();
item.put("faceId", record.faceId);
item.put("employeeNo", record.employeeNo);
item.put("name", record.name);
item.put("feature", encode(record.feature));
array.put(item);
}
FileOutputStream output = null;
try {
output = atomicStore.startWrite();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, UTF8));
writer.write(array.toString());
writer.flush();
atomicStore.finishWrite(output);
} catch (Exception error) {
if (output != null) {
atomicStore.failWrite(output);
}
throw error;
}
}
private static String encode(float[] feature) {
ByteBuffer buffer = ByteBuffer.allocate(feature.length * 4).order(ByteOrder.LITTLE_ENDIAN);
for (float value : feature) {
buffer.putFloat(value);
}
return Base64.encodeToString(buffer.array(), Base64.NO_WRAP);
}
private static float[] decode(String value) {
try {
byte[] bytes = Base64.decode(value, Base64.DEFAULT);
if (bytes.length == 0 || bytes.length % 4 != 0) {
return new float[0];
}
ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
float[] feature = new float[bytes.length / 4];
for (int i = 0; i < feature.length; i += 1) {
feature[i] = buffer.getFloat();
}
return feature;
} catch (Exception ignored) {
return new float[0];
}
}
private static float cosine(float[] first, float[] second) {
double dot = 0;
double firstNorm = 0;
double secondNorm = 0;
for (int i = 0; i < first.length; i += 1) {
dot += first[i] * second[i];
firstNorm += first[i] * first[i];
secondNorm += second[i] * second[i];
}
if (firstNorm <= 0 || secondNorm <= 0) {
return -1f;
}
return (float) (dot / Math.sqrt(firstNorm * secondNorm));
}
private static String safe(String value) {
return value == null ? "" : value.trim();
}
}
@@ -0,0 +1,543 @@
package com.xingyuan.zhiling;
import android.content.Context;
import android.content.res.AssetManager;
import android.hardware.Camera;
import android.os.SystemClock;
import android.util.Log;
import android.view.SurfaceHolder;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
final class FaceRecognitionController implements Camera.PreviewCallback {
private static final String TAG = "XingyuanFace";
interface Listener {
void onFaceStatus(String status, String message, int enrolledCount);
void onFaceRecognized(FaceFeatureStore.Match match);
}
private static final String MODEL_DIRECTORY = "face_models";
private static final String DETECTOR_MODEL = "face_detector.csta";
private static final String LANDMARKER_MODEL = "face_landmarker_pts5.csta";
private static final String RECOGNIZER_MODEL = "face_recognizer.csta";
private static final float DEFAULT_MATCH_THRESHOLD = 0.75f;
private static final long FRAME_INTERVAL_MS = 700;
private static final long RECOGNITION_COOLDOWN_MS = 5000;
private static final long ENROLLMENT_FEATURE_MAX_AGE_MS = 8000;
private static final long FEEDBACK_INTERVAL_MS = 1200;
private static final long CAMERA_RETRY_INTERVAL_MS = 5000;
private final Context context;
private final Listener listener;
private final FaceFeatureStore featureStore;
private final ExecutorService worker = Executors.newSingleThreadExecutor();
private final AtomicBoolean processing = new AtomicBoolean(false);
private final AtomicBoolean cameraTaskQueued = new AtomicBoolean(false);
private final AtomicBoolean destroyed = new AtomicBoolean(false);
private volatile Camera camera;
private volatile SurfaceHolder previewHolder;
private volatile boolean cameraStartRequestedWhileQueued;
private volatile boolean restartCameraWhenSurfaceReady;
private volatile boolean requestedActive;
private volatile long recognitionSession;
private volatile long lastCameraStartAttemptAt;
private volatile boolean engineReady;
private volatile int previewWidth;
private volatile int previewHeight;
private long lastFrameAt;
private long lastRecognizedAt;
private long lastSimilarityLogAt;
private long lastExtractionLogAt;
private long lastFeedbackAt;
private String lastFeedbackMessage = "";
private volatile float[] latestFeature;
private volatile long latestFeatureAt;
private volatile float matchThreshold = DEFAULT_MATCH_THRESHOLD;
private boolean faceAvailableNotified;
FaceRecognitionController(Context context, Listener listener) {
this.context = context.getApplicationContext();
this.listener = listener;
this.featureStore = new FaceFeatureStore(context);
// 模型约 100MB,复制和 SeetaFace6 初始化必须离开 UI 线程,否则首次启动会 ANR。
worker.execute(new Runnable() {
@Override
public void run() {
initializeEngine();
}
});
}
int enrolledCount() {
return featureStore.size();
}
String peopleJson() {
return featureStore.listPeople().toString();
}
synchronized float setMatchThreshold(float value) {
if (Float.isNaN(value) || Float.isInfinite(value)) {
value = DEFAULT_MATCH_THRESHOLD;
}
matchThreshold = Math.max(0.50f, Math.min(0.95f, value));
lastFeedbackAt = 0;
lastFeedbackMessage = "";
Log.i(TAG, "match threshold updated=" + matchThreshold);
return matchThreshold;
}
float getMatchThreshold() {
return matchThreshold;
}
synchronized boolean removePerson(String faceId) throws Exception {
boolean removed = featureStore.remove(faceId);
if (removed) {
notifyStatus("ready", "人员已删除", featureStore.size());
}
return removed;
}
void setActive(boolean active) {
if (destroyed.get()) {
return;
}
if (requestedActive == active) {
if (active && camera == null) {
requestCameraStart(false);
}
return;
}
requestedActive = active;
recognitionSession++;
latestFeature = null;
latestFeatureAt = 0;
faceAvailableNotified = false;
if (active) {
lastCameraStartAttemptAt = 0;
lastFrameAt = 0;
lastRecognizedAt = 0;
lastSimilarityLogAt = 0;
lastExtractionLogAt = 0;
lastFeedbackAt = 0;
lastFeedbackMessage = "";
Log.i(TAG, "resume recognition session=" + recognitionSession
+ " enrolled=" + featureStore.size() + " cameraOpen=" + (camera != null));
requestCameraStart(false);
} else {
// RK3399 的旧 Camera HAL 在页面切换时调用 stopPreview/release 可能阻塞数十秒。
// 普通页面切换只暂停帧处理,保留摄像头预览;Activity 销毁时才真正释放。
Log.i(TAG, "pause recognition session=" + recognitionSession);
}
}
boolean isRequestedActive() {
return requestedActive;
}
void retryAfterPermission() {
if (destroyed.get()) {
return;
}
if (requestedActive) {
lastCameraStartAttemptAt = 0;
requestCameraStart(false);
}
}
void setPreviewSurface(SurfaceHolder holder) {
if (holder == null || destroyed.get()) {
return;
}
boolean changed = previewHolder != holder;
previewHolder = holder;
if (requestedActive && (camera == null || (changed && restartCameraWhenSurfaceReady))) {
requestCameraStart(changed && restartCameraWhenSurfaceReady);
}
}
void clearPreviewSurface(SurfaceHolder holder) {
if (previewHolder != holder) {
return;
}
previewHolder = null;
if (camera != null) {
// 页面切换不会再销毁 Surface;若系统确实回收了 Surface,等新 Surface
// 创建后在后台线程重启摄像头,绝不在 UI 回调中调用 stopPreview。
restartCameraWhenSurfaceReady = true;
}
}
void destroy() {
if (!destroyed.compareAndSet(false, true)) {
return;
}
requestedActive = false;
recognitionSession++;
restartCameraWhenSurfaceReady = false;
previewHolder = null;
latestFeature = null;
latestFeatureAt = 0;
try {
worker.execute(new Runnable() {
@Override
public void run() {
stopCamera();
if (engineReady) {
try {
SeetaFaceNative.release();
} catch (Throwable ignored) {
}
engineReady = false;
}
}
});
} catch (Throwable ignored) {
}
worker.shutdown();
}
synchronized void enrollCurrent(String faceId, String employeeNo, String name) throws Exception {
float[] snapshot = latestFeature;
long featureAge = SystemClock.elapsedRealtime() - latestFeatureAt;
if (snapshot == null || snapshot.length == 0 || featureAge > ENROLLMENT_FEATURE_MAX_AGE_MS) {
String reason = safeNativeError();
if (reason.length() == 0 || "识别引擎初始化失败".equals(reason)) {
reason = "请正对摄像头,避免逆光并保持面部清晰";
}
throw new IllegalStateException("最近 8 秒内未检测到可录入人脸:" + reason);
}
featureStore.put(faceId, employeeNo, name, snapshot);
notifyStatus("ready", "人脸采集成功", featureStore.size());
}
synchronized void prepareEnrollment() {
latestFeature = null;
latestFeatureAt = 0;
faceAvailableNotified = false;
notifyStatus("searching", "请正对摄像头,正在采集新的 SeetaFace6 人脸特征", featureStore.size());
}
private void initializeEngine() {
try {
File modelDirectory = new File(context.getFilesDir(), MODEL_DIRECTORY);
copyAsset(DETECTOR_MODEL, modelDirectory);
copyAsset(LANDMARKER_MODEL, modelDirectory);
copyAsset(RECOGNIZER_MODEL, modelDirectory);
engineReady = SeetaFaceNative.initialize(
new File(modelDirectory, DETECTOR_MODEL).getAbsolutePath(),
new File(modelDirectory, LANDMARKER_MODEL).getAbsolutePath(),
new File(modelDirectory, RECOGNIZER_MODEL).getAbsolutePath());
notifyStatus(engineReady ? "ready" : "error",
engineReady ? "识别引擎已就绪" : safeNativeError(), featureStore.size());
} catch (Throwable error) {
engineReady = false;
notifyStatus("error", "识别引擎加载失败:" + message(error), featureStore.size());
}
}
private void requestCameraStart(final boolean restart) {
if (destroyed.get()) {
return;
}
if (restart) {
restartCameraWhenSurfaceReady = true;
}
SurfaceHolder holder = previewHolder;
if (!requestedActive || holder == null || holder.getSurface() == null
|| !holder.getSurface().isValid()) {
return;
}
long now = SystemClock.elapsedRealtime();
if (!restart && camera == null && lastCameraStartAttemptAt > 0
&& now - lastCameraStartAttemptAt < CAMERA_RETRY_INTERVAL_MS) {
return;
}
if (!cameraTaskQueued.compareAndSet(false, true)) {
cameraStartRequestedWhileQueued = true;
return;
}
lastCameraStartAttemptAt = now;
cameraStartRequestedWhileQueued = false;
try {
worker.execute(new Runnable() {
@Override
public void run() {
try {
boolean shouldRestart = restartCameraWhenSurfaceReady;
restartCameraWhenSurfaceReady = false;
if (shouldRestart) {
stopCamera();
}
startCamera();
} finally {
boolean retryRequested = cameraStartRequestedWhileQueued;
cameraStartRequestedWhileQueued = false;
cameraTaskQueued.set(false);
if (requestedActive && previewHolder != null
&& (retryRequested || restartCameraWhenSurfaceReady)) {
requestCameraStart(false);
}
}
}
});
} catch (Throwable ignored) {
cameraTaskQueued.set(false);
lastCameraStartAttemptAt = 0;
}
}
private synchronized void startCamera() {
if (destroyed.get() || !requestedActive || camera != null) {
return;
}
SurfaceHolder holder = previewHolder;
if (holder == null || holder.getSurface() == null || !holder.getSurface().isValid()) {
Log.i(TAG, "wait for persistent preview surface session=" + recognitionSession);
return;
}
Log.i(TAG, "open camera session=" + recognitionSession);
try {
int count = Camera.getNumberOfCameras();
if (count <= 0) {
notifyStatus("camera_error", "未发现系统摄像头", featureStore.size());
return;
}
camera = Camera.open(0);
Camera.Parameters parameters = camera.getParameters();
Camera.Size selected = choosePreviewSize(parameters.getSupportedPreviewSizes());
if (selected != null) {
parameters.setPreviewSize(selected.width, selected.height);
}
parameters.setPreviewFormat(android.graphics.ImageFormat.NV21);
camera.setParameters(parameters);
Camera.Size actual = camera.getParameters().getPreviewSize();
previewWidth = actual.width;
previewHeight = actual.height;
camera.setPreviewDisplay(holder);
camera.setPreviewCallback(this);
camera.startPreview();
notifyStatus(engineReady ? "recognizing" : "camera_ready",
engineReady
? "摄像头已连接 " + previewWidth + "×" + previewHeight + ",识别中"
: "摄像头已连接,但识别引擎未就绪",
featureStore.size());
} catch (SecurityException error) {
stopCamera();
notifyStatus("permission", "等待摄像头权限", featureStore.size());
} catch (Throwable error) {
stopCamera();
notifyStatus("camera_error", "摄像头启动失败:" + message(error), featureStore.size());
}
}
private synchronized void stopCamera() {
recognitionSession++;
Log.i(TAG, "stop recognition session=" + recognitionSession);
Camera current = camera;
camera = null;
if (current != null) {
try {
current.setPreviewCallback(null);
current.stopPreview();
} catch (Throwable ignored) {
}
try {
current.release();
} catch (Throwable ignored) {
}
}
latestFeature = null;
latestFeatureAt = 0;
faceAvailableNotified = false;
}
@Override
public void onPreviewFrame(final byte[] data, Camera source) {
if (destroyed.get() || !requestedActive || !engineReady
|| data == null || previewWidth <= 0 || previewHeight <= 0) {
return;
}
long now = SystemClock.elapsedRealtime();
if (now - lastFrameAt < FRAME_INTERVAL_MS || !processing.compareAndSet(false, true)) {
return;
}
lastFrameAt = now;
final byte[] frame = data.clone();
final int width = previewWidth;
final int height = previewHeight;
final long session = recognitionSession;
try {
worker.execute(new Runnable() {
@Override
public void run() {
try {
long extractionStartedAt = SystemClock.elapsedRealtime();
boolean shouldLogExtraction = extractionStartedAt - lastExtractionLogAt >= 3000;
if (shouldLogExtraction) {
lastExtractionLogAt = extractionStartedAt;
Log.i(TAG, "extract begin session=" + session + " size=" + width + "x" + height
+ " enrolled=" + featureStore.size());
}
float[] feature = SeetaFaceNative.extractFeature(frame, width, height, 0, false);
if (shouldLogExtraction) {
Log.i(TAG, "extract end session=" + session
+ " elapsedMs=" + (SystemClock.elapsedRealtime() - extractionStartedAt)
+ " feature=" + (feature == null ? 0 : feature.length)
+ " nativeError=" + safeNativeError());
}
if (!isRecognitionSessionCurrent(session)) {
Log.i(TAG, "discard stale frame session=" + session
+ " current=" + recognitionSession);
return;
}
if (feature == null || feature.length == 0) {
notifyFaceFeedback("face_error", safeNativeError());
if (latestFeatureAt > 0
&& SystemClock.elapsedRealtime() - latestFeatureAt > ENROLLMENT_FEATURE_MAX_AGE_MS) {
latestFeature = null;
latestFeatureAt = 0;
if (faceAvailableNotified) {
faceAvailableNotified = false;
notifyStatus("searching", "正在等待清晰正脸", featureStore.size());
}
}
return;
}
latestFeature = feature;
latestFeatureAt = SystemClock.elapsedRealtime();
if (!faceAvailableNotified) {
faceAvailableNotified = true;
notifyFaceFeedback("face_ready", "已检测到清晰正脸");
}
long recognizedNow = SystemClock.elapsedRealtime();
FaceFeatureStore.Match best = featureStore.findBest(feature, -1f);
float currentThreshold = matchThreshold;
if (best != null && recognizedNow - lastSimilarityLogAt >= 3000) {
lastSimilarityLogAt = recognizedNow;
Log.i(TAG, "best face=" + best.faceId + " similarity=" + best.similarity
+ " threshold=" + currentThreshold);
}
FaceFeatureStore.Match match = best != null && best.similarity >= currentThreshold ? best : null;
if (match == null) {
if (best == null) {
notifyFaceFeedback("no_match", "本地没有可匹配的人脸信息");
} else {
int similarityPercent = Math.round(best.similarity * 100f);
int thresholdPercent = Math.round(currentThreshold * 100f);
notifyFaceFeedback("no_match", "人脸匹配失败:相似度 "
+ similarityPercent + "% ,需要达到 " + thresholdPercent + "%");
}
}
if (match != null && recognizedNow - lastRecognizedAt >= RECOGNITION_COOLDOWN_MS) {
lastRecognizedAt = recognizedNow;
Log.i(TAG, "recognized face=" + match.faceId + " similarity=" + match.similarity);
if (isRecognitionSessionCurrent(session)) {
listener.onFaceRecognized(match);
}
}
} catch (Throwable error) {
notifyStatus("error", "人脸识别失败:" + message(error), featureStore.size());
} finally {
processing.set(false);
}
}
});
} catch (Throwable error) {
// Activity 销毁和 Camera 回调可能同时发生,线程池关闭后不能让拒绝异常逃出 Camera 线程。
processing.set(false);
if (!destroyed.get()) {
Log.w(TAG, "discard preview frame because worker is unavailable", error);
}
}
}
private boolean isRecognitionSessionCurrent(long session) {
return requestedActive && recognitionSession == session;
}
private void notifyFaceFeedback(String status, String message) {
String normalized = message == null || message.length() == 0 ? "人脸识别失败" : message;
long now = SystemClock.elapsedRealtime();
boolean sameMessage = normalized.equals(lastFeedbackMessage);
if (now - lastFeedbackAt < FEEDBACK_INTERVAL_MS
|| (sameMessage && now - lastFeedbackAt < FEEDBACK_INTERVAL_MS * 3)) {
return;
}
lastFeedbackAt = now;
lastFeedbackMessage = normalized;
notifyStatus(status, normalized, featureStore.size());
}
private void copyAsset(String name, File directory) throws Exception {
if (!directory.exists() && !directory.mkdirs()) {
throw new IllegalStateException("无法创建模型目录");
}
File target = new File(directory, name);
AssetManager assets = context.getAssets();
InputStream input = assets.open(MODEL_DIRECTORY + "/" + name);
int assetSize = input.available();
if (target.isFile() && assetSize > 0 && target.length() == assetSize) {
input.close();
return;
}
FileOutputStream output = new FileOutputStream(target, false);
try {
byte[] buffer = new byte[16384];
int count;
while ((count = input.read(buffer)) >= 0) {
output.write(buffer, 0, count);
}
} finally {
try {
input.close();
} finally {
output.close();
}
}
}
private Camera.Size choosePreviewSize(List<Camera.Size> sizes) {
if (sizes == null || sizes.isEmpty()) {
return null;
}
Camera.Size best = sizes.get(0);
long bestDistance = Long.MAX_VALUE;
for (Camera.Size size : sizes) {
long distance = Math.abs(size.width - 640L) + Math.abs(size.height - 480L);
if (distance < bestDistance) {
best = size;
bestDistance = distance;
}
}
return best;
}
private void notifyStatus(String status, String message, int count) {
if (!destroyed.get()) {
listener.onFaceStatus(status, message, count);
}
}
private String safeNativeError() {
try {
String value = SeetaFaceNative.lastError();
return value == null || value.length() == 0 ? "识别引擎初始化失败" : value;
} catch (Throwable ignored) {
return "识别引擎初始化失败";
}
}
private static String message(Throwable error) {
String value = error == null ? "未知错误" : error.getMessage();
return value == null || value.length() == 0 ? error.getClass().getSimpleName() : value;
}
}
@@ -0,0 +1,663 @@
package com.xingyuan.zhiling;
import android.Manifest;
import android.annotation.TargetApi;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.Settings;
import android.view.View;
import android.view.Gravity;
import android.view.Window;
import android.view.WindowManager;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.webkit.ValueCallback;
import android.webkit.RenderProcessGoneDetail;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import java.io.File;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import org.json.JSONObject;
public class MainActivity extends Activity implements
ZhitouSerialPortManager.Listener,
ZhitouTcpReporter.Listener,
FaceRecognitionController.Listener {
static final int FILE_CHOOSER_REQUEST = 4101;
private static final int PERMISSION_REQUEST = 4102;
private FrameLayout rootLayout;
private WebView webView;
private SurfaceView facePreviewView;
private int facePreviewLeft = Integer.MIN_VALUE;
private int facePreviewTop = Integer.MIN_VALUE;
private int facePreviewWidth = Integer.MIN_VALUE;
private int facePreviewHeight = Integer.MIN_VALUE;
private volatile boolean requestedFacePreviewVisible;
private volatile int requestedFacePreviewLeft;
private volatile int requestedFacePreviewTop;
private volatile int requestedFacePreviewWidth;
private volatile int requestedFacePreviewHeight;
private final AtomicBoolean facePreviewLayoutQueued = new AtomicBoolean(false);
private final AtomicLong facePreviewLayoutVersion = new AtomicLong(0L);
private String pendingRecoveryNotice;
private boolean recoveryRetryStarted;
private final Runnable markStableLaunchTask = new Runnable() {
@Override
public void run() {
CrashRecovery.markLaunchSuccessful(MainActivity.this);
}
};
private ValueCallback<Uri[]> filePathCallback;
private ZhitouTcpReporter tcpReporter;
private ZhitouSerialPortManager serialPortManager;
private ZhitouBridge bridge;
private FaceRecognitionController faceRecognitionController;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (CrashRecovery.shouldEnterStartupRecovery(this)) {
showStartupRecoveryScreen();
return;
}
pendingRecoveryNotice = CrashRecovery.consumeRecoveryNotice(this);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
rootLayout = new FrameLayout(this);
webView = new WebView(this);
tcpReporter = new ZhitouTcpReporter(this, this);
serialPortManager = new ZhitouSerialPortManager(this);
webView.setBackgroundColor(Color.rgb(247, 250, 248));
rootLayout.addView(webView, new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT));
facePreviewView = new SurfaceView(this);
facePreviewView.setZOrderOnTop(true);
facePreviewView.getHolder().setFormat(PixelFormat.OPAQUE);
// Surface 始终保留。RK3399 的旧 Camera HAL 在页面切换时重新绑定
// Surface 会阻塞主线程并触发 ANR;隐藏时只把画面缩到 1×1。
facePreviewView.setVisibility(View.VISIBLE);
rootLayout.addView(facePreviewView, new FrameLayout.LayoutParams(1, 1));
setContentView(rootLayout);
configureWebView();
requestRuntimePermissions();
prepareSmartStorage();
webView.loadUrl("file:///android_asset/www/index.html");
}
@SuppressLint("SetJavaScriptEnabled")
private void configureWebView() {
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setDatabaseEnabled(true);
settings.setAllowFileAccess(true);
settings.setAllowContentAccess(true);
settings.setLoadWithOverviewMode(true);
settings.setUseWideViewPort(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
// 页面只通过受控的原生桥通讯,不允许 file:// 页面任意读取文件或跨域访问。
settings.setAllowFileAccessFromFileURLs(false);
settings.setAllowUniversalAccessFromFileURLs(false);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
webView.setWebViewClient(new RecoveryWebViewClient());
webView.setWebChromeClient(new ZhitouWebChromeClient(this));
faceRecognitionController = new FaceRecognitionController(this, this);
facePreviewView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
if (faceRecognitionController != null) {
faceRecognitionController.setPreviewSurface(holder);
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (faceRecognitionController != null) {
faceRecognitionController.clearPreviewSurface(holder);
}
}
});
bridge = new ZhitouBridge(this, tcpReporter, serialPortManager, faceRecognitionController);
webView.addJavascriptInterface(bridge, "XingyuanNative");
webView.addJavascriptInterface(bridge, "ZhitouNative");
}
private void requestRuntimePermissions() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return;
}
String[] permissions = new String[] {
Manifest.permission.CAMERA,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
requestPermissions(permissions, PERMISSION_REQUEST);
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST) {
prepareSmartStorage();
if (faceRecognitionController != null) {
faceRecognitionController.retryAfterPermission();
}
}
}
private void prepareSmartStorage() {
File smartDir = new File(Environment.getExternalStorageDirectory(), "smart");
File imageDir = new File(smartDir, "Image");
File logsDir = new File(smartDir, "logs");
// 首次启动时创建业务固定目录,避免用户手动新建 smart/Image、smart/logs。
ensureDirectory(imageDir);
ensureDirectory(logsDir);
}
private void ensureDirectory(File dir) {
if (dir == null || dir.exists()) {
return;
}
try {
dir.mkdirs();
} catch (Exception ignored) {
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode != FILE_CHOOSER_REQUEST || filePathCallback == null) {
return;
}
Uri[] results = null;
if (resultCode == RESULT_OK && data != null) {
if (data.getClipData() != null) {
int count = data.getClipData().getItemCount();
results = new Uri[count];
for (int i = 0; i < count; i += 1) {
results[i] = data.getClipData().getItemAt(i).getUri();
}
} else if (data.getData() != null) {
results = new Uri[] { data.getData() };
}
}
filePathCallback.onReceiveValue(results);
filePathCallback = null;
}
void setFilePathCallback(ValueCallback<Uri[]> callback) {
if (filePathCallback != null) {
filePathCallback.onReceiveValue(null);
}
filePathCallback = callback;
}
void clearFilePathCallback() {
filePathCallback = null;
}
void launchFileChooser(Intent intent) throws ActivityNotFoundException {
intent.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(intent, FILE_CHOOSER_REQUEST);
}
void openSystemSettings() {
Intent intent = new Intent(Settings.ACTION_SETTINGS);
startActivity(intent);
}
void openFileManager() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
try {
startActivity(intent);
} catch (ActivityNotFoundException error) {
openFileManagerFallback();
}
}
@Override
public void onImei(String imei) {
dispatchSerialImei(imei);
}
@Override
public void onVersion(String version) {
dispatchSerialVersion(version);
}
@Override
public void onIdentity(int type, String identity) {
dispatchSerialIdentity(type, identity);
}
@Override
public void onThrowWeight(ZhitouSerialPortManager.ThrowWeightItem item) {
dispatchSerialThrowWeight(item);
}
@Override
public void onDeviceStatus(ZhitouSerialPortManager.DeviceStatusItem item) {
dispatchSerialDeviceStatus(item);
}
@Override
public void onReporterLog(String kind, String message) {
dispatchNativeLog(kind, message);
}
private void dispatchSerialIdentity(int type, String identity) {
if (webView == null) {
return;
}
runOnUiThread(new ZhitouSerialIdentityTask(webView, type, identity));
}
private void dispatchSerialImei(String imei) {
if (webView == null) {
return;
}
runOnUiThread(new ZhitouSerialImeiTask(webView, imei));
}
private void dispatchSerialVersion(String version) {
if (webView == null) {
return;
}
runOnUiThread(new ZhitouSerialVersionTask(webView, version));
}
private void dispatchSerialThrowWeight(ZhitouSerialPortManager.ThrowWeightItem item) {
if (webView == null || item == null) {
return;
}
runOnUiThread(new ZhitouSerialThrowWeightTask(webView, item));
}
private void dispatchSerialDeviceStatus(ZhitouSerialPortManager.DeviceStatusItem item) {
if (webView == null || item == null) {
return;
}
runOnUiThread(new ZhitouSerialDeviceStatusTask(webView, item));
}
private void dispatchNativeLog(String kind, String message) {
if (webView == null) {
return;
}
runOnUiThread(new ZhitouNativeLogTask(webView, kind, message));
}
@Override
public void onFaceStatus(String status, String message, int enrolledCount) {
if (webView == null) {
return;
}
runOnUiThread(new ZhitouFaceStatusTask(webView, status, message, enrolledCount));
}
@Override
public void onFaceRecognized(FaceFeatureStore.Match match) {
if (webView == null || match == null) {
return;
}
runOnUiThread(new ZhitouFaceRecognizedTask(webView, match));
}
void setFaceRecognitionActive(boolean active) {
if (faceRecognitionController != null) {
faceRecognitionController.setActive(active);
}
}
void setFacePreviewLayout(final boolean visible, final int left, final int top, final int width, final int height) {
requestedFacePreviewVisible = visible;
requestedFacePreviewLeft = left;
requestedFacePreviewTop = top;
requestedFacePreviewWidth = width;
requestedFacePreviewHeight = height;
facePreviewLayoutVersion.incrementAndGet();
queueFacePreviewLayoutApply();
}
private void queueFacePreviewLayoutApply() {
if (!facePreviewLayoutQueued.compareAndSet(false, true)) {
return;
}
runOnUiThread(new Runnable() {
@Override
public void run() {
long appliedVersion = facePreviewLayoutVersion.get();
applyFacePreviewLayout(
requestedFacePreviewVisible,
requestedFacePreviewLeft,
requestedFacePreviewTop,
requestedFacePreviewWidth,
requestedFacePreviewHeight);
facePreviewLayoutQueued.set(false);
if (facePreviewLayoutVersion.get() != appliedVersion) {
queueFacePreviewLayoutApply();
}
}
});
}
private void applyFacePreviewLayout(boolean visible, int left, int top, int width, int height) {
if (facePreviewView == null || rootLayout == null) {
return;
}
int availableWidth = rootLayout.getWidth();
int availableHeight = rootLayout.getHeight();
if (availableWidth <= 0 || availableHeight <= 0) {
availableWidth = getResources().getDisplayMetrics().widthPixels;
availableHeight = getResources().getDisplayMetrics().heightPixels;
}
availableWidth = Math.max(1, availableWidth);
availableHeight = Math.max(1, availableHeight);
boolean showPreview = visible && width > 0 && height > 0;
int targetLeft = showPreview ? clamp(left, 0, availableWidth - 1) : 0;
int targetTop = showPreview ? clamp(top, 0, availableHeight - 1) : 0;
int targetWidth = showPreview ? clamp(width, 1, availableWidth - targetLeft) : 1;
int targetHeight = showPreview ? clamp(height, 1, availableHeight - targetTop) : 1;
boolean layoutChanged = facePreviewLeft != targetLeft
|| facePreviewTop != targetTop
|| facePreviewWidth != targetWidth
|| facePreviewHeight != targetHeight;
if (layoutChanged) {
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(targetWidth, targetHeight);
params.leftMargin = targetLeft;
params.topMargin = targetTop;
facePreviewView.setLayoutParams(params);
facePreviewLeft = targetLeft;
facePreviewTop = targetTop;
facePreviewWidth = targetWidth;
facePreviewHeight = targetHeight;
if (showPreview) {
facePreviewView.bringToFront();
}
}
if (facePreviewView.getVisibility() != View.VISIBLE) {
facePreviewView.setVisibility(View.VISIBLE);
}
}
private static int clamp(int value, int minimum, int maximum) {
return Math.max(minimum, Math.min(maximum, value));
}
int getEnrolledFaceCount() {
return faceRecognitionController == null ? 0 : faceRecognitionController.enrolledCount();
}
float setFaceMatchThreshold(float value) {
return faceRecognitionController == null ? value : faceRecognitionController.setMatchThreshold(value);
}
float getFaceMatchThreshold() {
return faceRecognitionController == null ? 0.75f : faceRecognitionController.getMatchThreshold();
}
String getFacePeopleJson() {
return faceRecognitionController == null ? "[]" : faceRecognitionController.peopleJson();
}
boolean removeFacePerson(String faceId) throws Exception {
if (faceRecognitionController == null) {
throw new IllegalStateException("人脸识别控制器不可用");
}
return faceRecognitionController.removePerson(faceId);
}
void enrollCurrentFace(String faceId, String employeeNo, String name) throws Exception {
if (faceRecognitionController == null) {
throw new IllegalStateException("人脸识别控制器不可用");
}
faceRecognitionController.enrollCurrent(faceId, employeeNo, name);
}
void prepareFaceEnrollment() {
if (faceRecognitionController != null) {
faceRecognitionController.prepareEnrollment();
}
}
void dispatchConnectionCheck(JSONObject payload) {
if (webView == null) {
return;
}
runOnUiThread(new ZhitouConnectionCheckTask(webView, payload));
}
private void openFileManagerFallback() {
Intent fallback = new Intent(Intent.ACTION_GET_CONTENT);
fallback.setType("*/*");
fallback.addCategory(Intent.CATEGORY_OPENABLE);
try {
startActivity(fallback);
} catch (ActivityNotFoundException error) {
Toast.makeText(this, "无法打开文件管理", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onBackPressed() {
if (webView != null && webView.canGoBack()) {
webView.goBack();
return;
}
super.onBackPressed();
}
@Override
protected void onResume() {
super.onResume();
enterImmersiveMode();
}
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
if (level >= TRIM_MEMORY_MODERATE && webView != null) {
webView.freeMemory();
}
}
@Override
public void onLowMemory() {
super.onLowMemory();
if (webView != null) {
webView.freeMemory();
}
}
@Override
protected void onDestroy() {
if (faceRecognitionController != null) {
faceRecognitionController.destroy();
faceRecognitionController = null;
}
if (facePreviewView != null) {
facePreviewView.setVisibility(View.GONE);
}
if (bridge != null) {
bridge.destroy();
bridge = null;
}
if (webView != null) {
webView.removeCallbacks(markStableLaunchTask);
webView.removeJavascriptInterface("XingyuanNative");
webView.removeJavascriptInterface("ZhitouNative");
webView.stopLoading();
webView.loadUrl("about:blank");
webView.setWebChromeClient(null);
webView.setWebViewClient(null);
if (rootLayout != null) {
rootLayout.removeView(webView);
}
webView.destroy();
webView = null;
}
if (tcpReporter != null) {
tcpReporter.stop();
}
if (serialPortManager != null) {
serialPortManager.destroy();
}
super.onDestroy();
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
enterImmersiveMode();
}
}
private void enterImmersiveMode() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
}
}
private void showStartupRecoveryScreen() {
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
LinearLayout panel = new LinearLayout(this);
panel.setOrientation(LinearLayout.VERTICAL);
panel.setGravity(Gravity.CENTER);
panel.setPadding(64, 64, 64, 64);
panel.setBackgroundColor(Color.rgb(247, 250, 248));
TextView title = new TextView(this);
title.setText("应用安全恢复");
title.setTextSize(28f);
title.setTextColor(Color.rgb(29, 68, 51));
title.setGravity(Gravity.CENTER);
panel.addView(title, new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT));
TextView detail = new TextView(this);
detail.setText("上次启动期间发生异常,已阻止循环崩溃。\n\n"
+ CrashRecovery.peekRecoveryNotice(this));
detail.setTextSize(18f);
detail.setTextColor(Color.DKGRAY);
detail.setGravity(Gravity.CENTER);
LinearLayout.LayoutParams detailParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
detailParams.setMargins(0, 40, 0, 40);
panel.addView(detail, detailParams);
Button retry = new Button(this);
retry.setText("重新启动应用");
retry.setTextSize(20f);
retry.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
retryAfterStartupCrash();
}
});
panel.addView(retry, new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT));
setContentView(panel);
enterImmersiveMode();
if (CrashRecovery.canAutoRetryStartup(this)) {
panel.postDelayed(new Runnable() {
@Override
public void run() {
retryAfterStartupCrash();
}
}, 4_000L);
}
}
private void retryAfterStartupCrash() {
if (recoveryRetryStarted) {
return;
}
recoveryRetryStarted = true;
CrashRecovery.clearStartupRecoveryForRetry(this);
Intent retryIntent = new Intent(this, MainActivity.class);
retryIntent.setAction("com.xingyuan.zhiling.action.CRASH_RECOVERY");
retryIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
finish();
startActivity(retryIntent);
}
private final class RecoveryWebViewClient extends WebViewClient {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
view.removeCallbacks(markStableLaunchTask);
view.postDelayed(markStableLaunchTask, 30_000L);
if (pendingRecoveryNotice == null || pendingRecoveryNotice.length() == 0) {
return;
}
String notice = pendingRecoveryNotice;
pendingRecoveryNotice = "";
new ZhitouNativeLogTask(view, "恢复", notice).run();
Toast.makeText(MainActivity.this, notice, Toast.LENGTH_LONG).show();
}
@TargetApi(Build.VERSION_CODES.O)
@Override
public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) {
boolean didCrash = detail != null && detail.didCrash();
CrashRecovery.recordRendererCrashAndRestart(MainActivity.this, didCrash);
android.os.Process.killProcess(android.os.Process.myPid());
return true;
}
}
@SuppressWarnings("unused")
private void openApplicationSettings() {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
}
}
@@ -0,0 +1,23 @@
package com.xingyuan.zhiling;
final class SeetaFaceNative {
static {
System.loadLibrary("TenniS");
System.loadLibrary("SeetaAuthorize");
System.loadLibrary("SeetaFaceDetector600");
System.loadLibrary("SeetaFaceLandmarker600");
System.loadLibrary("SeetaFaceRecognizer600");
System.loadLibrary("xingyuan_face");
}
private SeetaFaceNative() {
}
static native boolean initialize(String detectorModel, String landmarkerModel, String recognizerModel);
static native float[] extractFeature(byte[] nv21, int width, int height, int rotationDegrees, boolean mirror);
static native String lastError();
static native void release();
}
@@ -0,0 +1,47 @@
package com.xingyuan.zhiling;
import android.app.Application;
import android.os.Process;
import android.util.Log;
import java.util.concurrent.atomic.AtomicBoolean;
public final class XingyuanApplication extends Application {
private static final String TAG = "XingyuanCrash";
private final AtomicBoolean handlingCrash = new AtomicBoolean(false);
@Override
public void onCreate() {
super.onCreate();
final Thread.UncaughtExceptionHandler previous = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable error) {
if (!handlingCrash.compareAndSet(false, true)) {
terminate(previous, thread, error);
return;
}
try {
Log.e(TAG, "Uncaught exception in " + thread.getName(), error);
CrashRecovery.recordAndSchedule(XingyuanApplication.this, error);
} catch (Throwable recoveryError) {
Log.e(TAG, "Unable to record or schedule crash recovery", recoveryError);
}
Process.killProcess(Process.myPid());
System.exit(10);
}
});
}
private static void terminate(
Thread.UncaughtExceptionHandler previous,
Thread thread,
Throwable error) {
if (previous != null) {
previous.uncaughtException(thread, error);
return;
}
Process.killProcess(Process.myPid());
System.exit(10);
}
}
@@ -0,0 +1,988 @@
package com.xingyuan.zhiling;
import android.os.Environment;
import android.webkit.JavascriptInterface;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Locale;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.json.JSONArray;
import org.json.JSONObject;
final class ZhitouBridge {
private static final class ConnectionCheckTask implements Runnable {
private final ZhitouBridge bridge;
private final String json;
ConnectionCheckTask(ZhitouBridge bridge, String json) {
this.bridge = bridge;
this.json = json;
}
@Override
public void run() {
bridge.runConnectionCheck(json);
}
}
private static final class SerialCheckTask implements Callable<Boolean> {
private final ZhitouSerialPortManager manager;
private final String path;
private final int baudRate;
SerialCheckTask(ZhitouSerialPortManager manager, String path, int baudRate) {
this.manager = manager;
this.path = path;
this.baudRate = baudRate;
}
@Override
public Boolean call() {
return manager.checkSerial(path, baudRate);
}
}
private static final class TcpCheckTask implements Callable<Boolean> {
private final ZhitouBridge bridge;
private final String host;
private final int port;
private final int timeoutSeconds;
TcpCheckTask(ZhitouBridge bridge, String host, int port, int timeoutSeconds) {
this.bridge = bridge;
this.host = host;
this.port = port;
this.timeoutSeconds = timeoutSeconds;
}
@Override
public Boolean call() {
return host.length() > 0 && port > 0 && bridge.connectTcp(host, port, timeoutSeconds);
}
}
private static final class PingCheckTask implements Callable<Boolean> {
private final ZhitouBridge bridge;
private final String host;
private final int timeoutSeconds;
PingCheckTask(ZhitouBridge bridge, String host, int timeoutSeconds) {
this.bridge = bridge;
this.host = host;
this.timeoutSeconds = timeoutSeconds;
}
@Override
public Boolean call() {
return bridge.ping(host.length() > 0 ? host : "www.baidu.com", timeoutSeconds);
}
}
private final MainActivity activity;
private final ZhitouTcpReporter reporter;
private final ZhitouSerialPortManager serialPortManager;
private final FaceRecognitionController faceRecognitionController;
private final ExecutorService backgroundExecutor = Executors.newFixedThreadPool(4);
private final AtomicBoolean destroyed = new AtomicBoolean(false);
ZhitouBridge(
MainActivity activity,
ZhitouTcpReporter reporter,
ZhitouSerialPortManager serialPortManager,
FaceRecognitionController faceRecognitionController) {
this.activity = activity;
this.reporter = reporter;
this.serialPortManager = serialPortManager;
this.faceRecognitionController = faceRecognitionController;
}
void destroy() {
if (!destroyed.compareAndSet(false, true)) {
return;
}
backgroundExecutor.shutdownNow();
}
private boolean executeBackground(Runnable task) {
if (destroyed.get()) {
return false;
}
try {
backgroundExecutor.execute(task);
return true;
} catch (RejectedExecutionException ignored) {
// WebView 回调可能与 Activity 销毁同时到达,此时直接丢弃过期任务。
return false;
}
}
@JavascriptInterface
public void configure(String json) {
try {
JSONObject data = obj(json);
reporter.configure(
data.optString("host", "112.74.41.228"),
data.optInt("port", 9956),
data.optString("terminalId", ""));
} catch (Exception ignored) {
}
}
@JavascriptInterface
public void startHeartbeat(String json) {
reporter.startHeartbeat();
}
@JavascriptInterface
public boolean pingHost(String json) {
try {
JSONObject data = obj(json);
String host = data.optString("host", "www.baidu.com").trim();
int timeoutSeconds = data.optInt("timeoutSeconds", 3);
if (host.length() == 0) {
host = "www.baidu.com";
}
return ping(host, timeoutSeconds);
} catch (Exception ignored) {
return false;
}
}
@JavascriptInterface
public boolean checkTcpServer(String json) {
try {
JSONObject data = obj(json);
String host = data.optString("host", "112.74.41.228").trim();
int port = data.optInt("port", 9956);
int timeoutSeconds = data.optInt("timeoutSeconds", 3);
if (host.length() == 0 || port <= 0) {
return false;
}
return connectTcp(host, port, timeoutSeconds);
} catch (Exception ignored) {
return false;
}
}
@JavascriptInterface
public String getLowerImei(String json) {
try {
JSONObject data = obj(json);
return serialPortManager.requestImei(
data.optString("path", "/dev/ttyS4"),
data.optInt("baudRate", 9600));
} catch (Exception ignored) {
return "";
}
}
@JavascriptInterface
public void requestLowerImei(String json) {
executeBackground(new ZhitouSerialImeiRequestTask(serialPortManager, json));
}
@JavascriptInterface
public boolean checkSerial(String json) {
try {
JSONObject data = obj(json);
return serialPortManager.checkSerial(
data.optString("path", "/dev/ttyS4"),
data.optInt("baudRate", 9600));
} catch (Exception ignored) {
return false;
}
}
@JavascriptInterface
public String getLowerVersion(String json) {
try {
JSONObject data = obj(json);
return serialPortManager.requestVersion(
data.optString("path", "/dev/ttyS4"),
data.optInt("baudRate", 9600));
} catch (Exception ignored) {
return "";
}
}
@JavascriptInterface
public void requestLowerVersion(String json) {
executeBackground(new ZhitouSerialVersionRequestTask(serialPortManager, json));
}
@JavascriptInterface
public void requestConnectionCheck(final String json) {
executeBackground(new ConnectionCheckTask(this, json));
}
private void runConnectionCheck(String json) {
JSONObject result = new JSONObject();
try {
final JSONObject data = obj(json);
final String path = data.optString("path", "/dev/ttyS4");
final int baudRate = data.optInt("baudRate", 9600);
final String host = data.optString("host", "112.74.41.228").trim();
final int port = data.optInt("port", 9956);
final int timeoutSeconds = Math.max(1, Math.min(data.optInt("timeoutSeconds", 2), 5));
final String pingHost = data.optString("pingHost", "www.baidu.com").trim();
final String mode = data.optString("mode", "startup");
Future<Boolean> serialFuture = null;
if (!"network".equals(mode)) {
serialFuture = backgroundExecutor.submit(new SerialCheckTask(serialPortManager, path, baudRate));
}
Future<Boolean> tcpFuture = backgroundExecutor.submit(new TcpCheckTask(this, host, port, timeoutSeconds));
Future<Boolean> pingFuture = backgroundExecutor.submit(new PingCheckTask(this, pingHost, timeoutSeconds));
boolean serialOnline = serialFuture == null || futureBoolean(serialFuture, timeoutSeconds + 1);
boolean tcpConnected = futureBoolean(tcpFuture, timeoutSeconds + 1);
boolean pingOk = futureBoolean(pingFuture, timeoutSeconds + 1);
result.put("requestId", data.optInt("requestId", 0));
result.put("mode", mode);
result.put("serialOnline", serialOnline);
result.put("tcpConnected", tcpConnected);
result.put("pingOk", pingOk);
result.put("networkOnline", tcpConnected || pingOk);
result.put("httpReachable", tcpConnected || pingOk);
} catch (Exception error) {
try {
result.put("mode", "startup");
result.put("error", error.getMessage() == null ? "connection check failed" : error.getMessage());
} catch (Exception ignored) {
}
}
if (!destroyed.get()) {
activity.dispatchConnectionCheck(result);
}
}
private boolean futureBoolean(Future<Boolean> future, int timeoutSeconds) {
try {
return Boolean.TRUE.equals(future.get(timeoutSeconds, TimeUnit.SECONDS));
} catch (Exception ignored) {
future.cancel(true);
return false;
}
}
@JavascriptInterface
public boolean openThrowDoor(String json) {
try {
JSONObject data = obj(json);
return serialPortManager.openThrowDoor(
data.optString("path", "/dev/ttyS4"),
data.optInt("baudRate", 9600),
data.optInt("port", 0));
} catch (Exception ignored) {
return false;
}
}
@JavascriptInterface
public boolean closeThrowDoor(String json) {
try {
JSONObject data = obj(json);
return serialPortManager.closeThrowDoor(
data.optString("path", "/dev/ttyS4"),
data.optInt("baudRate", 9600));
} catch (Exception ignored) {
return false;
}
}
@JavascriptInterface
public boolean openReceiveDoor(String json) {
try {
JSONObject data = obj(json);
return serialPortManager.openReceiveDoor(
data.optString("path", "/dev/ttyS4"),
data.optInt("baudRate", 9600),
data.optInt("port", 1));
} catch (Exception ignored) {
return false;
}
}
@JavascriptInterface
public boolean queryWeight(String json) {
try {
JSONObject data = obj(json);
return serialPortManager.queryWeight(
data.optString("path", "/dev/ttyS4"),
data.optInt("baudRate", 9600),
data.optInt("port", 0));
} catch (Exception ignored) {
return false;
}
}
@JavascriptInterface
public boolean queryFill(String json) {
try {
JSONObject data = obj(json);
return serialPortManager.queryFill(
data.optString("path", "/dev/ttyS4"),
data.optInt("baudRate", 9600),
data.optInt("port", 0));
} catch (Exception ignored) {
return false;
}
}
@JavascriptInterface
public boolean queryTemperature(String json) {
try {
JSONObject data = obj(json);
return serialPortManager.queryTemperature(
data.optString("path", "/dev/ttyS4"),
data.optInt("baudRate", 9600),
data.optInt("port", 0));
} catch (Exception ignored) {
return false;
}
}
@JavascriptInterface
public void openSystemSettings(String json) {
activity.runOnUiThread(new ZhitouOpenSystemSettingsTask(activity));
}
@JavascriptInterface
public void openFileManager(String json) {
activity.runOnUiThread(new ZhitouOpenFileManagerTask(activity));
}
@JavascriptInterface
public void reportMemberVerify(String json) {
try {
JSONObject data = obj(json);
reporter.reportMemberVerify(data.optInt("idType", 2), data.optString("idContent", ""));
} catch (Exception ignored) {
}
}
@JavascriptInterface
public String verifyMember(String json) {
try {
JSONObject data = obj(json);
return reporter.requestMemberVerify(
data.optInt("idType", 2),
data.optString("idContent", "")).toJson();
} catch (Exception ignored) {
return ZhitouMemberInfo.failed("会员鉴权异常").toJson();
}
}
@JavascriptInterface
public String fetchWorkContext(String json) {
// 正式机台接口未接入前返回空数组,前端仍允许手动填写,不能注入测试人员或批次。
return new JSONArray().toString();
}
/** 人脸平台接口尚未接入;明确返回失败,避免把测试人员写入真实人脸库。 */
@JavascriptInterface
public String syncFaceData(String json) {
JSONObject result = new JSONObject();
try {
result.put("ok", false);
result.put("count", activity.getEnrolledFaceCount());
result.put("error", "人员同步接口暂未接入");
} catch (Exception ignored) {
}
return result.toString();
}
@JavascriptInterface
public String verifyFace(String json) {
try {
String faceId = obj(json).optString("faceId", "").trim();
// 识别结果只允许在真实 1:N 特征库中核验,不能使用独立的测试名单缓存。
JSONArray faces = new JSONArray(activity.getFacePeopleJson());
for (int i = 0; i < faces.length(); i++) {
JSONObject face = faces.optJSONObject(i);
if (face != null && faceId.equals(face.optString("faceId"))) {
JSONObject result = new JSONObject();
result.put("ok", true);
result.put("employeeNo", face.optString("employeeNo"));
result.put("memberCode", face.optString("employeeNo"));
result.put("memberId", face.optString("employeeNo"));
result.put("name", face.optString("name"));
result.put("balance", 0);
return result.toString();
}
}
return ZhitouMemberInfo.failed("人脸识别失败").toJson();
} catch (Exception ignored) {
return ZhitouMemberInfo.failed("人脸识别失败").toJson();
}
}
@JavascriptInterface
public void setFaceRecognitionActive(String json) {
try {
activity.setFaceRecognitionActive(obj(json).optBoolean("active", false));
} catch (Exception ignored) {
}
}
@JavascriptInterface
public void setFacePreviewLayout(String json) {
try {
JSONObject data = obj(json);
activity.setFacePreviewLayout(
data.optBoolean("visible", false),
data.optInt("left", 0),
data.optInt("top", 0),
data.optInt("width", 0),
data.optInt("height", 0));
} catch (Exception ignored) {
}
}
@JavascriptInterface
public String getFaceRecognitionStatus(String json) {
JSONObject result = new JSONObject();
try {
result.put("ok", faceRecognitionController != null);
result.put("enrolledCount", activity.getEnrolledFaceCount());
result.put("matchThreshold", activity.getFaceMatchThreshold());
} catch (Exception ignored) {
}
return result.toString();
}
@JavascriptInterface
public String setFaceMatchThreshold(String json) {
JSONObject result = new JSONObject();
try {
float requested = (float) obj(json).optDouble("threshold", 0.75);
float applied = activity.setFaceMatchThreshold(requested);
result.put("ok", true);
result.put("threshold", applied);
} catch (Exception error) {
try {
result.put("ok", false);
result.put("error", "人脸匹配阈值设置失败");
} catch (Exception ignored) {
}
}
return result.toString();
}
@JavascriptInterface
public String listFacePeople(String json) {
JSONObject result = new JSONObject();
try {
JSONArray people = new JSONArray(activity.getFacePeopleJson());
result.put("ok", true);
result.put("people", people);
result.put("count", people.length());
} catch (Exception error) {
try {
result.put("ok", false);
result.put("people", new JSONArray());
result.put("error", "读取人员列表失败");
} catch (Exception ignored) {
}
}
return result.toString();
}
@JavascriptInterface
public String deleteFacePerson(String json) {
JSONObject result = new JSONObject();
try {
String faceId = obj(json).optString("faceId", "").trim();
if (faceId.length() == 0) {
throw new IllegalArgumentException("人员编号不能为空");
}
boolean removed = activity.removeFacePerson(faceId);
result.put("ok", removed);
result.put("count", activity.getEnrolledFaceCount());
if (!removed) {
result.put("error", "未找到该人员");
}
} catch (Exception error) {
try {
result.put("ok", false);
result.put("error", error.getMessage() == null ? "删除人员失败" : error.getMessage());
} catch (Exception ignored) {
}
}
return result.toString();
}
@JavascriptInterface
public String enrollCurrentFace(String json) {
JSONObject result = new JSONObject();
try {
JSONObject request = obj(json);
String employeeNo = request.optString("employeeNo", "").trim();
String name = request.optString("name", "").trim();
String faceId = request.optString("faceId", "").trim();
if (employeeNo.length() == 0 || name.length() == 0) {
throw new IllegalArgumentException("请输入工号和姓名");
}
if (faceId.length() == 0) {
faceId = "FACE-" + employeeNo;
}
activity.enrollCurrentFace(faceId, employeeNo, name);
result.put("ok", true);
result.put("faceId", faceId);
result.put("employeeNo", employeeNo);
result.put("name", name);
result.put("count", activity.getEnrolledFaceCount());
} catch (Exception error) {
try {
result.put("ok", false);
result.put("error", error.getMessage() == null ? "人脸采集失败" : error.getMessage());
} catch (Exception ignored) {
}
}
return result.toString();
}
@JavascriptInterface
public boolean prepareFaceEnrollment(String json) {
try {
activity.prepareFaceEnrollment();
return true;
} catch (Exception ignored) {
return false;
}
}
@JavascriptInterface
public boolean reportXingyuanDelivery(String json) {
try {
JSONObject data = obj(json);
reporter.reportDelivery(
data.optInt("idType", 0),
data.optString("idContent", data.optString("employeeNo", "")),
data.optInt("port", 0),
data.optInt("typeCode", 0),
data.optInt("range", 0),
data.optDouble("weight", 0),
data.optDouble("beforeWeight", 0),
data.optDouble("afterWeight", 0),
data.optLong("createdAt", System.currentTimeMillis()));
return true;
} catch (Exception ignored) {
return false;
}
}
@JavascriptInterface
public void reportDelivery(String json) {
try {
JSONObject data = obj(json);
reporter.reportDelivery(
data.optInt("idType", 2),
data.optString("idContent", ""),
data.optInt("port", 0),
data.optInt("garbageType", 18),
data.optInt("range", 0),
data.optDouble("throwWeight", 0),
data.optDouble("beforeWeight", 0),
data.optDouble("afterWeight", 0),
data.optLong("createdAt", 0));
} catch (Exception ignored) {
}
}
@JavascriptInterface
public void reportDeviceStatus(String json) {
try {
JSONObject data = obj(json);
reporter.reportDeviceStatus(data);
} catch (Exception ignored) {
}
}
@JavascriptInterface
public void reportAlarm(String json) {
try {
JSONObject data = obj(json);
JSONArray alarms = data.optJSONArray("alarms");
if (alarms != null) {
reporter.reportAlarm(alarms);
}
} catch (Exception ignored) {
}
}
@JavascriptInterface
public String getLocalBackupPath(String json) {
File dir = firstWritableCacheDir();
if (dir == null) {
return "";
}
return new File(dir, "xingyuan_zhiling_delivery_backup.csv").getAbsolutePath();
}
@JavascriptInterface
public String saveLocalBackup(String json) {
try {
JSONObject data = obj(json);
JSONObject record = data.optJSONObject("record");
if (record == null) {
return "";
}
File dir = firstWritableCacheDir();
if (dir == null) {
return "";
}
File jsonFile = new File(dir, "xingyuan_zhiling_delivery_backup.json");
File jsonlFile = new File(dir, "xingyuan_zhiling_delivery_backup.jsonl");
File csvFile = new File(dir, "xingyuan_zhiling_delivery_backup.csv");
appendJsonRecord(jsonFile, record);
appendLine(jsonlFile, record.toString());
boolean needHeader = !csvFile.exists() || csvFile.length() == 0;
BufferedWriter writer = new BufferedWriter(new FileWriter(csvFile, true));
try {
if (needHeader) {
writer.write("设备编号,设备唯一识别码IMEI,设备名称,投放时间,员工姓名,员工工号,机台号,废料名,废料名ID,投放前重量,投放后重量,本次投放重量,投口,上传状态");
writer.newLine();
}
writer.write(csv(record.optString("deviceNo"))
+ "," + csv(record.optString("deviceUniqueId"))
+ "," + csv(record.optString("deviceName"))
+ "," + csv(record.optString("deliveryTime", record.optString("time")))
+ "," + csv(record.optString("person"))
+ "," + csv(record.optString("employeeNo"))
+ "," + csv(record.optString("machineNo"))
+ "," + csv(record.optString("garbageTypeName"))
+ "," + csv(record.optString("typeId"))
+ "," + csv(record.optString("beforeWeight"))
+ "," + csv(record.optString("afterWeight"))
+ "," + csv(record.optString("weight"))
+ "," + csv(record.optString("port"))
+ "," + csv(record.optString("uploadStatus")));
writer.newLine();
} finally {
writer.close();
}
return csvFile.getAbsolutePath();
} catch (Exception ignored) {
return "";
}
}
@JavascriptInterface
public String exportLogs(String json) {
try {
JSONObject data = obj(json);
JSONArray logs = data.optJSONArray("logs");
File dir = firstWritableLogDir();
if (dir == null) {
return "";
}
String suffix = String.valueOf(data.optLong("exportedAt", System.currentTimeMillis()));
File jsonFile = new File(dir, "xingyuan_zhiling_logs_" + suffix + ".json");
File textFile = new File(dir, "xingyuan_zhiling_logs_" + suffix + ".txt");
BufferedWriter jsonWriter = new BufferedWriter(new FileWriter(jsonFile, false));
try {
jsonWriter.write(data.toString());
jsonWriter.newLine();
} finally {
jsonWriter.close();
}
BufferedWriter writer = new BufferedWriter(new FileWriter(textFile, false));
try {
writer.write("导出时间: " + data.optString("exportedAtText"));
writer.newLine();
writer.write("应用版本: " + data.optString("appVersion"));
writer.newLine();
writer.write("设备编号: " + data.optString("deviceNo"));
writer.newLine();
writer.write("终端号/IMEI: " + data.optString("terminalId"));
writer.newLine();
writer.write("--------------------------------------------------");
writer.newLine();
if (logs != null) {
for (int i = 0; i < logs.length(); i += 1) {
JSONObject item = logs.optJSONObject(i);
if (item == null) {
continue;
}
writer.write(item.optLong("at") + " [" + item.optString("kind") + "] " + item.optString("message"));
String detail = item.optString("detail");
if (detail != null && detail.length() > 0) {
writer.write(" - " + detail);
}
writer.newLine();
}
}
} finally {
writer.close();
}
return textFile.getAbsolutePath();
} catch (Exception ignored) {
return "";
}
}
@JavascriptInterface
public String readLocalBackups(String json) {
for (File file : backupFileCandidates()) {
if (file == null || !file.exists() || !file.isFile()) {
continue;
}
JSONArray records = readBackupFile(file);
if (records != null) {
return records.toString();
}
}
return new JSONArray().toString();
}
@JavascriptInterface
public String getCaptureImageDir(String json) {
File dir = firstWritableCaptureImageDir();
if (dir == null) {
return "";
}
return dir.getAbsolutePath();
}
@JavascriptInterface
public String prepareCaptureImage(String json) {
try {
JSONObject data = obj(json);
File dir = firstWritableCaptureImageDir();
if (dir == null) {
return "";
}
String fileName = captureFileName(data);
JSONObject result = new JSONObject();
result.put("directory", dir.getAbsolutePath());
result.put("fileName", fileName);
result.put("path", new File(dir, fileName).getAbsolutePath());
return result.toString();
} catch (Exception ignored) {
return "";
}
}
private JSONObject obj(String json) throws Exception {
if (json == null || json.length() == 0) {
return new JSONObject();
}
return new JSONObject(json);
}
private File firstWritableCacheDir() {
return firstWritableDir(new File[] {
new File(Environment.getExternalStorageDirectory(), "smart/cache"),
appExternalDirectory("smart/cache")
});
}
private File firstWritableLogDir() {
return firstWritableDir(new File[] {
new File(Environment.getExternalStorageDirectory(), "smart/logs"),
appExternalDirectory("smart/logs")
});
}
private File firstWritableCaptureImageDir() {
return firstWritableDir(new File[] {
new File(Environment.getExternalStorageDirectory(), "smart/Image"),
appExternalDirectory("smart/Image")
});
}
private File appExternalDirectory(String child) {
File base = activity.getExternalFilesDir(null);
return base == null ? null : new File(base, child);
}
private File firstWritableDir(File[] candidates) {
for (File dir : candidates) {
if (dir == null) {
continue;
}
try {
if (!dir.exists() && !dir.mkdirs()) {
continue;
}
if (dir.isDirectory() && dir.canWrite()) {
return dir;
}
} catch (Exception ignored) {
}
}
return null;
}
private String captureFileName(JSONObject data) {
String fileName = data.optString("fileName", "").trim();
long createdAt = data.optLong("createdAt", System.currentTimeMillis());
int port = data.optInt("port", 0);
int camera = data.optInt("camera", 0);
if (fileName.length() == 0) {
fileName = "IMG_" + createdAt + "_P" + port + "_C" + camera + ".jpg";
}
fileName = fileName.replaceAll("[^A-Za-z0-9._-]", "_");
if (fileName.length() == 0) {
fileName = "IMG_" + System.currentTimeMillis() + ".jpg";
}
String lower = fileName.toLowerCase(Locale.ROOT);
if (!lower.endsWith(".jpg") && !lower.endsWith(".jpeg") && !lower.endsWith(".png")) {
fileName += ".jpg";
}
return fileName;
}
private File[] backupFileCandidates() {
return new File[] {
new File(Environment.getExternalStorageDirectory(), "smart/cache/xingyuan_zhiling_delivery_backup.json"),
appExternalDirectory("smart/cache/xingyuan_zhiling_delivery_backup.json"),
new File(Environment.getExternalStorageDirectory(), "smart/cache/xingyuan_zhiling_delivery_backup.jsonl"),
appExternalDirectory("smart/cache/xingyuan_zhiling_delivery_backup.jsonl"),
new File(Environment.getExternalStorageDirectory(), "smart/xingyuan_zhiling_delivery_backup.jsonl"),
appExternalDirectory("smart/xingyuan_zhiling_delivery_backup.jsonl")
};
}
private JSONArray readBackupFile(File file) {
JSONArray records = new JSONArray();
BufferedReader reader = null;
try {
if (file.getName().toLowerCase(Locale.ROOT).endsWith(".json")) {
JSONArray saved = new JSONArray(readText(file));
for (int i = 0; i < saved.length(); i += 1) {
JSONObject item = saved.optJSONObject(i);
if (item != null) {
records.put(item);
}
}
return records;
}
reader = new BufferedReader(new FileReader(file));
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.length() == 0) {
continue;
}
records.put(new JSONObject(line));
}
return records;
} catch (Exception ignored) {
return null;
} finally {
if (reader != null) {
try {
reader.close();
} catch (Exception ignored) {
}
}
}
}
private void appendJsonRecord(File file, JSONObject record) throws Exception {
JSONArray records = new JSONArray();
if (file.exists() && file.length() > 0) {
String text = readText(file).trim();
if (text.length() > 0) {
try {
records = new JSONArray(text);
} catch (Exception ignored) {
records = new JSONArray();
}
}
}
records.put(record);
BufferedWriter writer = new BufferedWriter(new FileWriter(file, false));
try {
writer.write(records.toString());
writer.newLine();
} finally {
writer.close();
}
}
private String readText(File file) throws Exception {
StringBuilder builder = new StringBuilder();
BufferedReader reader = new BufferedReader(new FileReader(file));
try {
String line;
while ((line = reader.readLine()) != null) {
builder.append(line);
}
} finally {
reader.close();
}
return builder.toString();
}
private void appendLine(File file, String line) throws Exception {
BufferedWriter writer = new BufferedWriter(new FileWriter(file, true));
try {
writer.write(line);
writer.newLine();
} finally {
writer.close();
}
}
private String csv(String value) {
String text = value == null ? "" : value;
return "\"" + text.replace("\"", "\"\"") + "\"";
}
private boolean ping(String host, int timeoutSeconds) {
int timeout = Math.max(1, Math.min(timeoutSeconds, 10));
String seconds = String.valueOf(timeout);
if (runPing(new String[] { "/system/bin/ping", "-c", "1", "-W", seconds, host })) {
return true;
}
return runPing(new String[] { "ping", "-c", "1", "-W", seconds, host });
}
private boolean connectTcp(String host, int port, int timeoutSeconds) {
Socket socket = null;
int timeoutMs = Math.max(1, Math.min(timeoutSeconds, 10)) * 1000;
try {
// 网络可用性以业务 TCP 可连为准,避免 ICMP ping 被系统或网络策略拦截后误报。
socket = new Socket();
socket.connect(new InetSocketAddress(host, port), timeoutMs);
return true;
} catch (Exception ignored) {
return false;
} finally {
if (socket != null) {
try {
socket.close();
} catch (Exception ignored) {
}
}
}
}
private boolean runPing(String[] command) {
Process process = null;
try {
process = Runtime.getRuntime().exec(command);
return process.waitFor() == 0;
} catch (Exception ignored) {
return false;
} finally {
if (process != null) {
process.destroy();
}
}
}
}
@@ -0,0 +1,22 @@
package com.xingyuan.zhiling;
import android.webkit.WebView;
import org.json.JSONObject;
final class ZhitouConnectionCheckTask implements Runnable {
private final WebView webView;
private final JSONObject payload;
ZhitouConnectionCheckTask(WebView webView, JSONObject payload) {
this.webView = webView;
this.payload = payload;
}
@Override
public void run() {
String js = "window.ZhitouSerial&&window.ZhitouSerial.onConnectionCheck("
+ (payload == null ? "{}" : payload.toString()) + ");";
webView.evaluateJavascript(js, null);
}
}
@@ -0,0 +1,14 @@
package com.xingyuan.zhiling;
final class ZhitouDeviceStatusTask implements Runnable {
private final ZhitouTcpReporter reporter;
ZhitouDeviceStatusTask(ZhitouTcpReporter reporter) {
this.reporter = reporter;
}
@Override
public void run() {
reporter.sendDeviceStatusNow();
}
}
@@ -0,0 +1,33 @@
package com.xingyuan.zhiling;
import android.webkit.WebView;
import org.json.JSONObject;
final class ZhitouFaceRecognizedTask implements Runnable {
private final WebView webView;
private final FaceFeatureStore.Match match;
ZhitouFaceRecognizedTask(WebView webView, FaceFeatureStore.Match match) {
this.webView = webView;
this.match = match;
}
@Override
public void run() {
if (webView == null || match == null) {
return;
}
try {
JSONObject payload = new JSONObject();
payload.put("faceId", match.faceId);
payload.put("employeeNo", match.employeeNo);
payload.put("name", match.name);
payload.put("similarity", match.similarity);
webView.evaluateJavascript(
"window.XingyuanSerial&&window.XingyuanSerial.onFaceRecognized(" + payload.toString() + ");",
null);
} catch (Exception ignored) {
}
}
}
@@ -0,0 +1,36 @@
package com.xingyuan.zhiling;
import android.webkit.WebView;
import org.json.JSONObject;
final class ZhitouFaceStatusTask implements Runnable {
private final WebView webView;
private final String status;
private final String message;
private final int enrolledCount;
ZhitouFaceStatusTask(WebView webView, String status, String message, int enrolledCount) {
this.webView = webView;
this.status = status;
this.message = message;
this.enrolledCount = enrolledCount;
}
@Override
public void run() {
if (webView == null) {
return;
}
try {
JSONObject payload = new JSONObject();
payload.put("status", status);
payload.put("message", message);
payload.put("enrolledCount", enrolledCount);
webView.evaluateJavascript(
"window.XingyuanSerial&&window.XingyuanSerial.onFaceStatus(" + payload.toString() + ");",
null);
} catch (Exception ignored) {
}
}
}
@@ -0,0 +1,14 @@
package com.xingyuan.zhiling;
final class ZhitouHeartbeatTask implements Runnable {
private final ZhitouTcpReporter reporter;
ZhitouHeartbeatTask(ZhitouTcpReporter reporter) {
this.reporter = reporter;
}
@Override
public void run() {
reporter.sendHeartbeatNow();
}
}
@@ -0,0 +1,40 @@
package com.xingyuan.zhiling;
import org.json.JSONObject;
final class ZhitouMemberInfo {
int verifyStatus;
String memberId = "";
String memberCode = "";
String name = "";
String balance = "";
int resiCount;
String error = "";
boolean isVerified() {
return verifyStatus == 1;
}
String toJson() {
JSONObject data = new JSONObject();
try {
data.put("ok", isVerified());
data.put("verifyStatus", verifyStatus);
data.put("memberId", memberId == null ? "" : memberId);
data.put("memberCode", memberCode == null ? "" : memberCode);
data.put("name", name == null ? "" : name);
data.put("balance", balance == null ? "" : balance);
data.put("resiCount", resiCount);
data.put("error", error == null ? "" : error);
} catch (Exception ignored) {
}
return data.toString();
}
static ZhitouMemberInfo failed(String error) {
ZhitouMemberInfo info = new ZhitouMemberInfo();
info.verifyStatus = 0;
info.error = error == null ? "" : error;
return info;
}
}
@@ -0,0 +1,24 @@
package com.xingyuan.zhiling;
import android.webkit.WebView;
import org.json.JSONObject;
final class ZhitouNativeLogTask implements Runnable {
private final WebView webView;
private final String kind;
private final String message;
ZhitouNativeLogTask(WebView webView, String kind, String message) {
this.webView = webView;
this.kind = kind == null ? "传输" : kind;
this.message = message == null ? "" : message;
}
@Override
public void run() {
String js = "window.ZhitouSerial&&window.ZhitouSerial.onNativeLog("
+ JSONObject.quote(kind) + "," + JSONObject.quote(message) + ");";
webView.evaluateJavascript(js, null);
}
}
@@ -0,0 +1,14 @@
package com.xingyuan.zhiling;
final class ZhitouOpenFileManagerTask implements Runnable {
private final MainActivity activity;
ZhitouOpenFileManagerTask(MainActivity activity) {
this.activity = activity;
}
@Override
public void run() {
activity.openFileManager();
}
}
@@ -0,0 +1,14 @@
package com.xingyuan.zhiling;
final class ZhitouOpenSystemSettingsTask implements Runnable {
private final MainActivity activity;
ZhitouOpenSystemSettingsTask(MainActivity activity) {
this.activity = activity;
}
@Override
public void run() {
activity.openSystemSettings();
}
}
@@ -0,0 +1,18 @@
package com.xingyuan.zhiling;
final class ZhitouSendFrameTask implements Runnable {
private final ZhitouTcpReporter reporter;
private final int orderCode;
private final byte[] body;
ZhitouSendFrameTask(ZhitouTcpReporter reporter, int orderCode, byte[] body) {
this.reporter = reporter;
this.orderCode = orderCode;
this.body = body.clone();
}
@Override
public void run() {
reporter.sendFrameNow(orderCode, body);
}
}
@@ -0,0 +1,32 @@
package com.xingyuan.zhiling;
import android.webkit.WebView;
import org.json.JSONObject;
final class ZhitouSerialDeviceStatusTask implements Runnable {
private final WebView webView;
private final ZhitouSerialPortManager.DeviceStatusItem item;
ZhitouSerialDeviceStatusTask(WebView webView, ZhitouSerialPortManager.DeviceStatusItem item) {
this.webView = webView;
this.item = item;
}
@Override
public void run() {
JSONObject payload = new JSONObject();
try {
payload.put("port", item.port);
if (!Double.isNaN(item.temperature)) {
payload.put("temperature", item.temperature);
}
if (item.range >= 0) {
payload.put("range", item.range);
}
} catch (Exception ignored) {
}
String js = "window.ZhitouSerial&&window.ZhitouSerial.onDeviceStatus(" + payload.toString() + ");";
webView.evaluateJavascript(js, null);
}
}
@@ -0,0 +1,24 @@
package com.xingyuan.zhiling;
import android.webkit.WebView;
import org.json.JSONObject;
final class ZhitouSerialIdentityTask implements Runnable {
private final WebView webView;
private final int type;
private final String identity;
ZhitouSerialIdentityTask(WebView webView, int type, String identity) {
this.webView = webView;
this.type = type;
this.identity = identity == null ? "" : identity;
}
@Override
public void run() {
String js = "window.ZhitouSerial&&window.ZhitouSerial.onIdentity("
+ type + "," + JSONObject.quote(identity) + ");";
webView.evaluateJavascript(js, null);
}
}
@@ -0,0 +1,24 @@
package com.xingyuan.zhiling;
import org.json.JSONObject;
final class ZhitouSerialImeiRequestTask implements Runnable {
private final ZhitouSerialPortManager serialPortManager;
private final String json;
ZhitouSerialImeiRequestTask(ZhitouSerialPortManager serialPortManager, String json) {
this.serialPortManager = serialPortManager;
this.json = json;
}
@Override
public void run() {
try {
JSONObject data = json == null || json.length() == 0 ? new JSONObject() : new JSONObject(json);
serialPortManager.requestImei(
data.optString("path", "/dev/ttyS4"),
data.optInt("baudRate", 9600));
} catch (Exception ignored) {
}
}
}
@@ -0,0 +1,22 @@
package com.xingyuan.zhiling;
import android.webkit.WebView;
import org.json.JSONObject;
final class ZhitouSerialImeiTask implements Runnable {
private final WebView webView;
private final String imei;
ZhitouSerialImeiTask(WebView webView, String imei) {
this.webView = webView;
this.imei = imei == null ? "" : imei;
}
@Override
public void run() {
String js = "window.ZhitouSerial&&window.ZhitouSerial.onImei("
+ JSONObject.quote(imei) + ");";
webView.evaluateJavascript(js, null);
}
}
@@ -0,0 +1,743 @@
package com.xingyuan.zhiling;
import android.util.Log;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import android_serialport_api.SerialPort;
final class ZhitouSerialPortManager {
private static final class SerialWriteTask implements Runnable {
private final ZhitouSerialPortManager manager;
private final byte[] frame;
private final long generation;
SerialWriteTask(ZhitouSerialPortManager manager, byte[] frame, long generation) {
this.manager = manager;
this.frame = frame;
this.generation = generation;
}
@Override
public void run() {
manager.writeFrame(frame, generation);
}
}
interface Listener {
void onImei(String imei);
void onVersion(String version);
void onIdentity(int type, String identity);
void onThrowWeight(ThrowWeightItem item);
void onDeviceStatus(DeviceStatusItem item);
}
static final class ThrowWeightItem {
final int port;
final double weight;
final double beforeWeight;
final double afterWeight;
final int range;
final boolean queryResponse;
ThrowWeightItem(int port, double weight, double beforeWeight, double afterWeight, int range,
boolean queryResponse) {
this.port = port;
this.weight = weight;
this.beforeWeight = beforeWeight;
this.afterWeight = afterWeight;
this.range = range;
this.queryResponse = queryResponse;
}
}
static final class DeviceStatusItem {
final int port;
final double temperature;
final int range;
DeviceStatusItem(int port, double temperature, int range) {
this.port = port;
this.temperature = temperature;
this.range = range;
}
}
private static final String TAG = "ZhitouSerial";
private static final String DEFAULT_PATH = "/dev/ttyS4";
private static final int DEFAULT_BAUD_RATE = 9600;
private static final long SERIAL_CHECK_WAIT_MS = 1800;
private static final long WRITE_GAP_MS = 40;
private static final Charset GBK = Charset.forName("GBK");
private static final byte ORDER_MEMBER_VERIFY = 0x01;
private static final byte ORDER_OPEN_THROW_DOOR = 0x02;
private static final byte ORDER_CLOSE_THROW_DOOR = 0x03;
private static final byte ORDER_OPEN_RECEIVE_DOOR = 0x04;
private static final byte ORDER_QUERY_WEIGHT = 0x20;
private static final byte ORDER_QUERY_FILL = 0x21;
private static final byte ORDER_QUERY_TEMPERATURE = 0x22;
private static final byte ORDER_DEVICE_VERSION = 0x30;
private static final byte ORDER_DEVICE_IMEI = 0x31;
private static final byte ORDER_CLOSE_TIME = 0x60;
private static final byte ORDER_THROW_WEIGHT = 0x61;
private static final byte ORDER_DEVICE_STATUS = 0x62;
private static final byte ORDER_GPS = 0x67;
private static final byte ORDER_POWER_ON = (byte) 0x90;
private final Listener listener;
private final Object ioLock = new Object();
private final Object imeiLock = new Object();
private final Object versionLock = new Object();
private final byte[] receiveBuffer = new byte[4096];
private final ExecutorService writeExecutor = Executors.newSingleThreadExecutor();
private SerialPort serialPort;
private InputStream inputStream;
private OutputStream outputStream;
private Thread readThread;
private String currentPath = DEFAULT_PATH;
private int currentBaudRate = DEFAULT_BAUD_RATE;
private int receiveLength;
private volatile boolean running;
private volatile String lastImei = "";
private volatile String lastVersion = "";
private volatile long lastValidFrameAt;
private volatile long connectionGeneration;
private volatile boolean destroyed;
ZhitouSerialPortManager(Listener listener) {
this.listener = listener;
}
boolean checkSerial(String path, int baudRate) {
long baseline;
if (!ensureOpen(path, baudRate)) {
return false;
}
if (hasRecentValidFrame()) {
return true;
}
baseline = lastValidFrameAt;
// 启动自检必须等到下位机真实回包,不能只用“串口能打开”判断在线。
if (!sendFrame(ORDER_DEVICE_IMEI, new byte[0])) {
return false;
}
return waitForValidFrameAfter(baseline, SERIAL_CHECK_WAIT_MS);
}
String requestImei(String path, int baudRate) {
if (!ensureOpen(path, baudRate)) {
return "";
}
synchronized (imeiLock) {
lastImei = "";
}
sendFrame(ORDER_DEVICE_IMEI, new byte[0]);
return waitForValue(imeiLock, true);
}
String requestVersion(String path, int baudRate) {
if (!ensureOpen(path, baudRate)) {
return "";
}
synchronized (versionLock) {
lastVersion = "";
}
sendFrame(ORDER_DEVICE_VERSION, new byte[0]);
return waitForValue(versionLock, false);
}
boolean openThrowDoor(String path, int baudRate, int port) {
if (!ensureOpen(path, baudRate)) {
return false;
}
return sendFrame(ORDER_OPEN_THROW_DOOR, new byte[] { 0x01, (byte) clamp(port, 0, 255) });
}
boolean closeThrowDoor(String path, int baudRate) {
if (!ensureOpen(path, baudRate)) {
return false;
}
return sendFrame(ORDER_CLOSE_THROW_DOOR, new byte[] { 0x01 });
}
boolean openReceiveDoor(String path, int baudRate, int doorIndex) {
if (!ensureOpen(path, baudRate)) {
return false;
}
return sendFrame(ORDER_OPEN_RECEIVE_DOOR, new byte[] { 0x01, (byte) clamp(doorIndex, 0, 255) });
}
boolean queryWeight(String path, int baudRate, int port) {
if (!ensureOpen(path, baudRate)) {
return false;
}
return sendFrame(ORDER_QUERY_WEIGHT, new byte[] { 0x01, (byte) clamp(port, 0, 255) });
}
boolean queryFill(String path, int baudRate, int port) {
if (!ensureOpen(path, baudRate)) {
return false;
}
return sendFrame(ORDER_QUERY_FILL, new byte[] { 0x01, (byte) clamp(port, 0, 255) });
}
boolean queryTemperature(String path, int baudRate, int port) {
if (!ensureOpen(path, baudRate)) {
return false;
}
return sendFrame(ORDER_QUERY_TEMPERATURE, new byte[] { 0x01, (byte) clamp(port, 0, 255) });
}
void close() {
synchronized (ioLock) {
connectionGeneration += 1;
running = false;
if (inputStream != null) {
try {
inputStream.close();
} catch (Exception ignored) {
}
}
inputStream = null;
if (outputStream != null) {
try {
outputStream.close();
} catch (Exception ignored) {
}
}
outputStream = null;
if (serialPort != null) {
try {
serialPort.close();
} catch (Exception ignored) {
}
}
serialPort = null;
readThread = null;
receiveLength = 0;
lastValidFrameAt = 0;
}
}
void destroy() {
destroyed = true;
close();
writeExecutor.shutdownNow();
}
private boolean ensureOpen(String path, int baudRate) {
if (destroyed) {
return false;
}
String nextPath = normalizePath(path);
int nextBaudRate = baudRate > 0 ? baudRate : DEFAULT_BAUD_RATE;
synchronized (ioLock) {
if (serialPort != null && outputStream != null && inputStream != null
&& nextPath.equals(currentPath) && nextBaudRate == currentBaudRate) {
return true;
}
close();
currentPath = nextPath;
currentBaudRate = nextBaudRate;
try {
serialPort = new SerialPort(new File(currentPath), currentBaudRate, 0);
inputStream = serialPort.getInputStream();
outputStream = serialPort.getOutputStream();
running = true;
long readGeneration = connectionGeneration;
readThread = new Thread(
new ZhitouSerialReadLoop(this, readGeneration),
"zhitou-serial-read");
readThread.setDaemon(true);
readThread.start();
Log.i(TAG, "opened " + currentPath + " baud=" + currentBaudRate);
return true;
} catch (Throwable error) {
Log.e(TAG, "open failed path=" + currentPath + " baud=" + currentBaudRate, error);
close();
return false;
}
}
}
private boolean sendFrame(byte orderCode, byte[] body) {
final byte[] frame = buildFrame(orderCode, body == null ? new byte[0] : body);
final long generation;
synchronized (ioLock) {
if (destroyed || outputStream == null) {
return false;
}
generation = connectionGeneration;
}
try {
writeExecutor.execute(new SerialWriteTask(this, frame, generation));
return true;
} catch (RejectedExecutionException error) {
Log.w(TAG, "send rejected because serial manager is shutting down");
return false;
}
}
private void writeFrame(byte[] frame, long generation) {
synchronized (ioLock) {
if (destroyed || generation != connectionGeneration || outputStream == null) {
return;
}
try {
outputStream.write(frame);
outputStream.flush();
Log.i(TAG, "send " + toHex(frame));
} catch (Exception error) {
Log.e(TAG, "send failed " + toHex(frame), error);
close();
return;
}
}
try {
Thread.sleep(WRITE_GAP_MS);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
private byte[] buildFrame(byte orderCode, byte[] body) {
byte[] result = new byte[body.length + 10];
int index = 0;
result[index++] = (byte) 0xfe;
result[index++] = (byte) 0xfe;
result[index++] = 0x68;
result[index++] = 0x00;
result[index++] = 0x68;
result[index++] = orderCode;
result[index++] = (byte) ((body.length >> 8) & 0xff);
result[index++] = (byte) (body.length & 0xff);
System.arraycopy(body, 0, result, index, body.length);
index += body.length;
result[index++] = checksum(orderCode, body);
result[index] = 0x16;
return result;
}
private byte checksum(byte orderCode, byte[] body) {
int sum = 0x68 + 0x00 + 0x68 + unsigned(orderCode);
sum += (body.length >> 8) & 0xff;
sum += body.length & 0xff;
for (byte b : body) {
sum += unsigned(b);
}
return (byte) sum;
}
private String waitForValue(Object lock, boolean imei) {
long deadline = System.currentTimeMillis() + 2200;
synchronized (lock) {
while ((imei ? lastImei : lastVersion).length() == 0) {
long remaining = deadline - System.currentTimeMillis();
if (remaining <= 0) {
break;
}
try {
lock.wait(remaining);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
break;
}
}
return imei ? lastImei : lastVersion;
}
}
boolean isRunning(long generation) {
return running && generation == connectionGeneration;
}
void readSerialOnce(long generation) {
if (!isRunning(generation)) {
return;
}
InputStream stream = inputStream;
if (stream == null) {
return;
}
try {
byte[] chunk = new byte[128];
int size = stream.read(chunk);
if (size > 0 && isRunning(generation)) {
appendReceived(chunk, size);
} else if (size < 0) {
// EOF 表示本次连接已经失效,必须退出读循环,避免空转占满 CPU。
closeIfCurrent(generation);
}
} catch (Exception error) {
if (isRunning(generation)) {
Log.e(TAG, "read failed", error);
closeIfCurrent(generation);
}
}
}
private void closeIfCurrent(long generation) {
synchronized (ioLock) {
if (generation != connectionGeneration) {
return;
}
// 连接代次用于隔离旧读线程,避免快速重连后两个线程同时读取新串口。
close();
}
}
private void appendReceived(byte[] chunk, int size) {
synchronized (receiveBuffer) {
if (size > receiveBuffer.length) {
size = receiveBuffer.length;
}
if (receiveLength + size > receiveBuffer.length) {
receiveLength = 0;
}
System.arraycopy(chunk, 0, receiveBuffer, receiveLength, size);
receiveLength += size;
Log.i(TAG, "recv " + toHex(chunk, size));
parseFrames();
}
}
private void parseFrames() {
int offset = 0;
while (true) {
int header = findHeader(offset);
if (header < 0) {
retainPartialHeader();
return;
}
if (header > 0) {
shiftLeft(header);
header = 0;
}
if (receiveLength < 8) {
return;
}
int length = (unsigned(receiveBuffer[4]) << 8) | unsigned(receiveBuffer[5]);
int total = 8 + length;
if (total > receiveBuffer.length) {
shiftLeft(1);
continue;
}
if (receiveLength < total) {
return;
}
if (receiveBuffer[total - 1] != 0x16) {
shiftLeft(1);
continue;
}
byte order = receiveBuffer[3];
byte[] body = new byte[length];
System.arraycopy(receiveBuffer, 6, body, 0, length);
byte actualChecksum = receiveBuffer[6 + length];
byte expectedChecksum = checksum(order, body);
if (actualChecksum == expectedChecksum) {
lastValidFrameAt = System.currentTimeMillis();
handleFrame(order, body);
} else {
Log.w(TAG, "checksum mismatch order=" + unsigned(order)
+ " expected=" + unsigned(expectedChecksum)
+ " actual=" + unsigned(actualChecksum));
}
shiftLeft(total);
}
}
private int findHeader(int offset) {
for (int i = offset; i + 2 < receiveLength; i += 1) {
if (receiveBuffer[i] == 0x68 && receiveBuffer[i + 1] == 0x00 && receiveBuffer[i + 2] == 0x68) {
return i;
}
}
return -1;
}
private void retainPartialHeader() {
int keep = 0;
if (receiveLength >= 2
&& receiveBuffer[receiveLength - 2] == 0x68
&& receiveBuffer[receiveLength - 1] == 0x00) {
keep = 2;
} else if (receiveLength >= 1 && receiveBuffer[receiveLength - 1] == 0x68) {
keep = 1;
}
if (keep > 0) {
System.arraycopy(receiveBuffer, receiveLength - keep, receiveBuffer, 0, keep);
}
receiveLength = keep;
}
private void shiftLeft(int count) {
if (count <= 0) {
return;
}
if (count >= receiveLength) {
receiveLength = 0;
return;
}
System.arraycopy(receiveBuffer, count, receiveBuffer, 0, receiveLength - count);
receiveLength -= count;
}
private void handleFrame(byte order, byte[] body) {
int code = unsigned(order);
if (code == unsigned(ORDER_MEMBER_VERIFY)) {
handleIdentity(body);
sendFrame(ORDER_MEMBER_VERIFY, new byte[0]);
} else if (code == unsigned(ORDER_DEVICE_IMEI)) {
handleImei(body);
} else if (code == unsigned(ORDER_DEVICE_VERSION)) {
handleVersion(body);
} else if (code == unsigned(ORDER_OPEN_RECEIVE_DOOR)) {
Log.i(TAG, "receive door command result");
} else if (code == unsigned(ORDER_QUERY_WEIGHT)) {
handleQueryWeight(body);
} else if (code == unsigned(ORDER_QUERY_FILL)) {
handleQueryFill(body);
} else if (code == unsigned(ORDER_QUERY_TEMPERATURE)) {
handleQueryTemperature(body);
} else if (code == unsigned(ORDER_THROW_WEIGHT)) {
handleThrowWeight(body);
sendFrame(ORDER_THROW_WEIGHT, new byte[0]);
} else if (code == unsigned(ORDER_DEVICE_STATUS)) {
sendFrame(ORDER_DEVICE_STATUS, new byte[0]);
handleDeviceStatus(body);
} else if (code == unsigned(ORDER_CLOSE_TIME)) {
sendFrame(ORDER_CLOSE_TIME, new byte[0]);
} else if (code == unsigned(ORDER_GPS)) {
sendFrame(ORDER_GPS, new byte[0]);
} else if (code == unsigned(ORDER_POWER_ON)) {
sendFrame(ORDER_POWER_ON, new byte[0]);
} else {
Log.i(TAG, "unhandled order=0x" + Integer.toHexString(code));
}
}
private void handleIdentity(byte[] body) {
if (body.length < 3) {
return;
}
int version = unsigned(body[0]);
if (version != 1) {
return;
}
int type = unsigned(body[1]);
int length = Math.min(unsigned(body[2]), body.length - 3);
String identity = new String(body, 3, Math.max(length, 0), GBK).trim();
if (identity.startsWith("http") && identity.contains("card=")) {
identity = identity.substring(identity.indexOf("card=") + 5);
}
if (identity.matches("(?i)^[0-9a-f]{8}$")) {
type = 1;
}
if (identity.length() > 0 && listener != null) {
listener.onIdentity(type, identity);
}
}
private void handleImei(byte[] body) {
if (body.length < 2) {
return;
}
int length = Math.min(unsigned(body[1]), body.length - 2);
String imei = new String(body, 2, Math.max(length, 0), GBK).trim();
synchronized (imeiLock) {
lastImei = imei;
imeiLock.notifyAll();
}
if (imei.length() > 0 && listener != null) {
listener.onImei(imei);
}
}
private void handleVersion(byte[] body) {
if (body.length < 2) {
return;
}
int length = Math.min(unsigned(body[1]), body.length - 2);
String version = new String(body, 2, Math.max(length, 0), GBK).trim();
synchronized (versionLock) {
lastVersion = version;
versionLock.notifyAll();
}
if (version.length() > 0 && listener != null) {
listener.onVersion(version);
}
}
private void handleThrowWeight(byte[] body) {
if (body.length < 2 || listener == null) {
Log.w(TAG, "invalid throw weight body length=" + body.length + " data=" + toHex(body));
return;
}
int count = unsigned(body[1]);
int expectedLength = 2 + count * 8;
if (body.length < expectedLength) {
Log.w(TAG, "invalid throw weight body count=" + count
+ " expected=" + expectedLength + " actual=" + body.length
+ " data=" + toHex(body));
return;
}
List<ThrowWeightItem> items = new ArrayList<>();
for (int i = 0; i < count; i += 1) {
int base = 2 + i * 8;
if (base + 7 >= body.length) {
break;
}
int port = unsigned(body[base]);
double weight = centWeight(body, base + 1);
double before = centWeight(body, base + 3);
double after = centWeight(body, base + 5);
int range = unsigned(body[base + 7]);
items.add(new ThrowWeightItem(port, weight, before, after, range, false));
}
for (ThrowWeightItem item : items) {
listener.onThrowWeight(item);
}
}
private void handleQueryWeight(byte[] body) {
if (body.length < 2 || listener == null) {
return;
}
int count = unsigned(body[1]);
for (int i = 0; i < count; i += 1) {
int base = 2 + i * 3;
if (base + 2 >= body.length) {
break;
}
int port = unsigned(body[base]);
double weight = centWeight(body, base + 1);
listener.onThrowWeight(new ThrowWeightItem(port, weight, weight, weight, -1, true));
}
}
private void handleQueryFill(byte[] body) {
if (body.length < 2 || listener == null) {
return;
}
int count = unsigned(body[1]);
for (int i = 0; i < count; i += 1) {
int base = 2 + i * 2;
if (base + 1 >= body.length) {
break;
}
listener.onDeviceStatus(new DeviceStatusItem(unsigned(body[base]), Double.NaN, unsigned(body[base + 1])));
}
}
private void handleQueryTemperature(byte[] body) {
if (body.length < 2 || listener == null) {
return;
}
int count = unsigned(body[1]);
for (int i = 0; i < count; i += 1) {
int base = 2 + i * 4;
if (base + 3 >= body.length) {
break;
}
listener.onDeviceStatus(new DeviceStatusItem(unsigned(body[base]), signedTemperature(body, base + 1), -1));
}
}
private void handleDeviceStatus(byte[] body) {
if (body.length < 2 || listener == null) {
return;
}
int count = unsigned(body[1]);
for (int i = 0; i < count; i += 1) {
int base = 2 + i * 5;
if (base + 4 >= body.length) {
break;
}
int port = unsigned(body[base]);
double temperature = signedTemperature(body, base + 1);
int range = unsigned(body[base + 4]);
listener.onDeviceStatus(new DeviceStatusItem(port, temperature, range));
}
}
private double centWeight(byte[] bytes, int index) {
int value = (unsigned(bytes[index]) << 8) | unsigned(bytes[index + 1]);
return new BigDecimal(value).divide(new BigDecimal(100)).doubleValue();
}
private double signedTemperature(byte[] bytes, int signIndex) {
int value = (unsigned(bytes[signIndex + 1]) << 8) | unsigned(bytes[signIndex + 2]);
BigDecimal temperature = new BigDecimal(value).divide(new BigDecimal(10));
return bytes[signIndex] == 0x01 ? temperature.negate().doubleValue() : temperature.doubleValue();
}
private String normalizePath(String path) {
if (path == null || path.trim().length() == 0) {
return DEFAULT_PATH;
}
return path.trim();
}
private boolean hasRecentValidFrame() {
long at = lastValidFrameAt;
return at > 0 && System.currentTimeMillis() - at <= 15000;
}
private boolean waitForValidFrameAfter(long baseline, long timeoutMs) {
long deadline = System.currentTimeMillis() + timeoutMs;
while (System.currentTimeMillis() < deadline) {
if (lastValidFrameAt > baseline) {
return true;
}
try {
Thread.sleep(40);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
}
private static int unsigned(byte value) {
return value & 0xff;
}
private static int clamp(int value, int min, int max) {
return Math.max(min, Math.min(max, value));
}
private static String toHex(byte[] data) {
return toHex(data, data.length);
}
private static String toHex(byte[] data, int size) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < size; i += 1) {
if (i > 0) {
builder.append(' ');
}
int value = data[i] & 0xff;
if (value < 16) {
builder.append('0');
}
builder.append(Integer.toHexString(value).toUpperCase(Locale.ROOT));
}
return builder.toString();
}
}
@@ -0,0 +1,18 @@
package com.xingyuan.zhiling;
final class ZhitouSerialReadLoop implements Runnable {
private final ZhitouSerialPortManager manager;
private final long generation;
ZhitouSerialReadLoop(ZhitouSerialPortManager manager, long generation) {
this.manager = manager;
this.generation = generation;
}
@Override
public void run() {
while (manager.isRunning(generation)) {
manager.readSerialOnce(generation);
}
}
}
@@ -0,0 +1,33 @@
package com.xingyuan.zhiling;
import android.webkit.WebView;
import org.json.JSONObject;
final class ZhitouSerialThrowWeightTask implements Runnable {
private final WebView webView;
private final ZhitouSerialPortManager.ThrowWeightItem item;
ZhitouSerialThrowWeightTask(WebView webView, ZhitouSerialPortManager.ThrowWeightItem item) {
this.webView = webView;
this.item = item;
}
@Override
public void run() {
JSONObject payload = new JSONObject();
try {
payload.put("port", item.port);
payload.put("weight", item.weight);
payload.put("beforeWeight", item.beforeWeight);
payload.put("afterWeight", item.afterWeight);
payload.put("queryResponse", item.queryResponse);
if (item.range >= 0) {
payload.put("range", item.range);
}
} catch (Exception ignored) {
}
String js = "window.ZhitouSerial&&window.ZhitouSerial.onThrowWeight(" + payload.toString() + ");";
webView.evaluateJavascript(js, null);
}
}
@@ -0,0 +1,24 @@
package com.xingyuan.zhiling;
import org.json.JSONObject;
final class ZhitouSerialVersionRequestTask implements Runnable {
private final ZhitouSerialPortManager serialPortManager;
private final String json;
ZhitouSerialVersionRequestTask(ZhitouSerialPortManager serialPortManager, String json) {
this.serialPortManager = serialPortManager;
this.json = json;
}
@Override
public void run() {
try {
JSONObject data = json == null || json.length() == 0 ? new JSONObject() : new JSONObject(json);
serialPortManager.requestVersion(
data.optString("path", "/dev/ttyS4"),
data.optInt("baudRate", 9600));
} catch (Exception ignored) {
}
}
}
@@ -0,0 +1,22 @@
package com.xingyuan.zhiling;
import android.webkit.WebView;
import org.json.JSONObject;
final class ZhitouSerialVersionTask implements Runnable {
private final WebView webView;
private final String version;
ZhitouSerialVersionTask(WebView webView, String version) {
this.webView = webView;
this.version = version == null ? "" : version;
}
@Override
public void run() {
String js = "window.ZhitouSerial&&window.ZhitouSerial.onVersion("
+ JSONObject.quote(version) + ");";
webView.evaluateJavascript(js, null);
}
}
@@ -0,0 +1,804 @@
package com.xingyuan.zhiling;
import android.content.Context;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.charset.Charset;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.Locale;
final class ZhitouTcpReporter {
interface Listener {
void onReporterLog(String kind, String message);
}
private static final String TAG = "ZhitouTcpReporter";
private static final Charset UTF8 = Charset.forName("UTF-8");
private static final Charset ASCII = Charset.forName("US-ASCII");
private static final byte[] HEADER = "HERO".getBytes(ASCII);
private static final int ORDER_HEART = 1001;
private static final int ORDER_MEMBER_INFO = 4301;
private static final int ORDER_MEMBER_INFO_RESP = 4302;
private static final int ORDER_DEVICE_STATUS_2003 = 2003;
private static final int ORDER_THROW_INFO = 6011;
private static final int ORDER_ALARM_EVENT = 6006;
private static final int DEVICE_TYPE_GARBAGE_CAN = 0x01;
private static final int HEARTBEAT_INTERVAL_SECONDS = 30;
private static final int STATUS_INTERVAL_SECONDS = 10 * 60;
private final Context context;
private final ExecutorService sender = Executors.newSingleThreadExecutor();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final Object socketLock = new Object();
private final Listener listener;
private final AtomicBoolean stopped = new AtomicBoolean(false);
private ScheduledFuture<?> heartbeatFuture;
private ScheduledFuture<?> statusFuture;
private Socket socket;
private InputStream inputStream;
private OutputStream outputStream;
private volatile String host = "112.74.41.228";
private volatile int port = 9956;
private volatile long terminalId;
private long lastDeviceStatusSentAt;
private int serialNum = 1;
ZhitouTcpReporter(Context context, Listener listener) {
this.context = context.getApplicationContext();
this.listener = listener;
this.terminalId = 0L;
}
synchronized void configure(String host, int port, String terminalIdText) {
if (stopped.get()) {
return;
}
boolean connectionChanged = false;
boolean terminalChanged = false;
if (host != null && host.trim().length() > 0) {
String nextHost = host.trim();
if (!nextHost.equals(this.host)) {
this.host = nextHost;
connectionChanged = true;
}
}
if (port > 0) {
if (this.port != port) {
this.port = port;
connectionChanged = true;
}
}
if (terminalIdText != null) {
long parsed = parseTerminalId(terminalIdText);
long nextTerminalId = parsed > 0 ? parsed : 0L;
if (nextTerminalId != this.terminalId) {
this.terminalId = nextTerminalId;
connectionChanged = true;
terminalChanged = true;
}
}
if (connectionChanged) {
closeSocketQuietly();
}
emitLog("传输", "配置 TCP " + this.host + ":" + this.port + " / IMEI " + getTerminalIdText());
if (terminalChanged && this.terminalId > 0) {
sendHeartbeatNow();
sendDeviceStatusNow();
}
}
synchronized void startHeartbeat() {
if (stopped.get()) {
return;
}
if (heartbeatFuture != null) {
heartbeatFuture.cancel(false);
}
sendHeartbeatNow();
heartbeatFuture = scheduler.scheduleAtFixedRate(
new ZhitouHeartbeatTask(this),
HEARTBEAT_INTERVAL_SECONDS,
HEARTBEAT_INTERVAL_SECONDS,
TimeUnit.SECONDS);
startDeviceStatusReporter();
emitLog("传输", "心跳任务已启动");
}
private synchronized void startDeviceStatusReporter() {
if (stopped.get()) {
return;
}
if (statusFuture != null) {
statusFuture.cancel(false);
}
sendDeviceStatusNow();
statusFuture = scheduler.scheduleAtFixedRate(
new ZhitouDeviceStatusTask(this),
STATUS_INTERVAL_SECONDS,
STATUS_INTERVAL_SECONDS,
TimeUnit.SECONDS);
emitLog("传输", "设备状态任务已启动");
}
void stop() {
if (!stopped.compareAndSet(false, true)) {
return;
}
if (heartbeatFuture != null) {
heartbeatFuture.cancel(false);
}
if (statusFuture != null) {
statusFuture.cancel(false);
}
scheduler.shutdownNow();
sender.shutdownNow();
// connect() 最长可能阻塞 10 退出 Activity 时异步收尾避免主线程被 socketLock 卡住
Thread closer = new Thread(new Runnable() {
@Override
public void run() {
closeSocketQuietly();
}
}, "zhitou-tcp-close");
closer.setDaemon(true);
closer.start();
}
String getTerminalIdText() {
return terminalId > 0 ? String.valueOf(terminalId) : "";
}
void reportMemberVerify(int idType, String idContent) {
sendFrameAsync(ORDER_MEMBER_INFO, buildMemberVerifyBody(idType, idContent));
}
ZhitouMemberInfo requestMemberVerify(int idType, String idContent) {
if (stopped.get()) {
return ZhitouMemberInfo.failed("网络服务已停止");
}
if (terminalId <= 0) {
emitLog("传输", "4301 跳过:未获取下位机IMEI");
return ZhitouMemberInfo.failed("未获取下位机IMEI");
}
byte[] body = buildMemberVerifyBody(idType, idContent);
byte[] frame = buildFrame(ORDER_MEMBER_INFO, body);
try {
synchronized (socketLock) {
ensureSocket();
socket.setSoTimeout(500);
outputStream.write(frame);
outputStream.flush();
emitTx(ORDER_MEMBER_INFO, frame);
long deadline = System.currentTimeMillis() + 8000;
while (System.currentTimeMillis() < deadline) {
ResponseFrame response = readResponseFrame(deadline);
if (response == null) {
continue;
}
emitRx(response);
if (response.orderCode == ORDER_MEMBER_INFO_RESP) {
return parseMemberInfo(response.body);
}
}
}
Log.w(TAG, "member verify timeout, closing socket and refreshing heartbeat");
emitLog("传输", "4301 超时:未收到 4302");
closeSocketQuietly();
sendHeartbeatNow();
return ZhitouMemberInfo.failed("会员资料返回超时");
} catch (Exception error) {
closeSocketQuietly();
Log.e(TAG, "member verify failed", error);
emitLog("传输", "4301 失败:" + error.getClass().getSimpleName());
sendHeartbeatNow();
return ZhitouMemberInfo.failed("会员鉴权通讯失败");
}
}
private byte[] buildMemberVerifyBody(int idType, String idContent) {
byte[] idBytes = fixedIdentityBytes(idType, idContent);
byte[] body = new byte[3 + 33];
int index = 0;
body[index++] = (byte) DEVICE_TYPE_GARBAGE_CAN;
body[index++] = (byte) idType;
body[index++] = (byte) Math.min(rawIdentityLength(idType, idContent), 33);
System.arraycopy(idBytes, 0, body, index, 33);
return body;
}
void reportDelivery(
int idType,
String idContent,
int throwIndex,
int garbageType,
int range,
double throwWeight,
double beforeWeight,
double afterWeight,
long createdAt) {
byte[] idBytes = fixedIdentityBytes(idType, idContent);
byte[] body = new byte[1 + 1 + 33 + 1 + 2 + 1 + 2 + 2];
int index = 0;
body[index++] = (byte) idType;
body[index++] = (byte) Math.min(rawIdentityLength(idType, idContent), 33);
System.arraycopy(idBytes, 0, body, index, 33);
index += 33;
body[index++] = (byte) garbageType;
putUInt16(body, index, kgToCentWeight(throwWeight));
index += 2;
body[index++] = (byte) clamp(range, 0, 255);
putUInt16(body, index, kgToCentWeight(beforeWeight));
index += 2;
putUInt16(body, index, kgToCentWeight(afterWeight));
sendFrameAsync(ORDER_THROW_INFO, body);
}
void reportDeviceStatus(JSONObject data) {
sendDeviceStatus(data);
}
void sendDeviceStatusNow() {
sendDeviceStatus(null);
}
private synchronized void sendDeviceStatus(JSONObject data) {
if (terminalId <= 0) {
sendFrameAsync(ORDER_DEVICE_STATUS_2003, buildDeviceStatusBody(data));
return;
}
long now = System.currentTimeMillis();
if (lastDeviceStatusSentAt > 0 && now - lastDeviceStatusSentAt < 60 * 1000) {
emitLog("传输", "设备状态 2003 跳过:60秒内已上报");
return;
}
lastDeviceStatusSentAt = now;
sendFrameAsync(ORDER_DEVICE_STATUS_2003, buildDeviceStatusBody(data));
}
private byte[] buildDeviceStatusBody(JSONObject data) {
if (data == null) {
data = new JSONObject();
}
JSONArray ports = data.optJSONArray("ports");
int count = ports == null ? 0 : ports.length();
if (count == 0) {
count = 2;
}
byte[] body = new byte[19 + count * 2];
int index = 0;
body[index++] = 0;
double temperature = data.optDouble("temperature", 0);
body[index++] = (byte) (temperature < 0 ? 1 : 0);
putUInt16(body, index, (int) Math.round(Math.abs(temperature) * 10));
index += 2;
body[index++] = (byte) clamp(data.optInt("humidity", 0), 0, 255);
body[index++] = (byte) clamp(data.optInt("signal", 30), 0, 255);
body[index++] = hasLocation(data) ? (byte) 1 : (byte) 0;
putUInt32(body, index, decimal4(data.optString("lng", "0")));
index += 4;
putUInt32(body, index, decimal4(data.optString("lat", "0")));
index += 4;
putUInt32(body, index, decimal4(data.optString("altitude", "0")));
index += 4;
for (int i = 0; i < count; i += 1) {
JSONObject item = ports == null ? null : ports.optJSONObject(i);
if (item == null) {
body[index++] = (byte) (i == 0 ? 3 : 1);
body[index++] = (byte) 150;
} else {
body[index++] = (byte) clamp(item.optInt("garbageType", 18), 0, 255);
body[index++] = (byte) clamp(item.optInt("range", 0), 0, 255);
}
}
return body;
}
void reportAlarm(JSONArray alarms) {
if (alarms.length() == 0) return;
boolean hot = false;
for (int i = 0; i < alarms.length(); i += 1) {
JSONObject alarm = alarms.optJSONObject(i);
if (alarm != null && alarm.optInt("errorCode", 0) == 311) {
hot = true;
break;
}
}
byte[] body = new byte[(hot ? 7 : 5) * alarms.length() + 2];
int index = 0;
body[index++] = 1;
body[index++] = (byte) alarms.length();
for (int i = 0; i < alarms.length(); i += 1) {
JSONObject alarm = alarms.optJSONObject(i);
if (alarm == null) alarm = new JSONObject();
int errorCode = alarm.optInt("errorCode", 301);
body[index++] = (byte) clamp(alarm.optInt("port", 0), 0, 255);
body[index++] = (byte) clamp(alarm.optInt("garbageType", 18), 0, 255);
putUInt16(body, index, errorCode);
index += 2;
body[index++] = (byte) clamp(alarm.optInt("isError", 1), 0, 1);
if (hot) {
if (errorCode == 311) {
putUInt16(body, index, (int) Math.round(alarm.optDouble("temperature", 0) * 10));
} else {
putUInt16(body, index, 0);
}
index += 2;
}
}
sendFrameAsync(ORDER_ALARM_EVENT, body);
}
void sendHeartbeatNow() {
byte[] body = new byte[1];
body[0] = (byte) DEVICE_TYPE_GARBAGE_CAN;
sendFrameAsync(ORDER_HEART, body);
}
private void sendFrameAsync(int orderCode, byte[] body) {
if (stopped.get()) {
return;
}
try {
sender.execute(new ZhitouSendFrameTask(this, orderCode, body));
} catch (RejectedExecutionException ignored) {
// Activity 销毁与页面回调并发发生时丢弃已经过期的上报任务
}
}
void sendFrameNow(int orderCode, byte[] body) {
if (stopped.get()) {
return;
}
if (terminalId <= 0) {
Log.w(TAG, "skip send order=" + orderCode + ", terminal imei not ready");
emitLog("传输", "TX " + orderCode + " 跳过:未获取下位机IMEI");
return;
}
byte[] frame = buildFrame(orderCode, body);
try {
synchronized (socketLock) {
writeFrameLocked(frame);
emitTx(orderCode, frame);
drainResponsesLocked(350);
}
Log.i(TAG, "sent order=" + orderCode + " bytes=" + frame.length + " to " + host + ":" + port);
} catch (Exception error) {
closeSocketQuietly();
Log.e(TAG, "send failed order=" + orderCode + " to " + host + ":" + port, error);
emitLog("传输", "TX " + orderCode + " 失败:" + errorSummary(error));
retryFrameOnce(orderCode, frame);
}
}
private void retryFrameOnce(int orderCode, byte[] frame) {
if (stopped.get()) {
return;
}
try {
synchronized (socketLock) {
writeFrameLocked(frame);
drainResponsesLocked(350);
}
Log.i(TAG, "retry sent order=" + orderCode + " bytes=" + frame.length + " to " + host + ":" + port);
emitLog("传输", "TX " + orderCode + " 重连重发成功 bytes=" + frame.length);
} catch (Exception retryError) {
closeSocketQuietly();
Log.e(TAG, "retry send failed order=" + orderCode + " to " + host + ":" + port, retryError);
emitLog("传输", "TX " + orderCode + " 重连重发失败:" + errorSummary(retryError));
}
}
private void writeFrameLocked(byte[] frame) throws Exception {
ensureSocket();
socket.setSoTimeout(500);
outputStream.write(frame);
outputStream.flush();
}
private void ensureSocket() throws Exception {
if (stopped.get()) {
throw new IOException("TCP reporter stopped");
}
if (socket != null && socket.isConnected() && !socket.isClosed()) {
return;
}
closeSocketQuietly();
Socket nextSocket = new Socket();
nextSocket.setKeepAlive(true);
nextSocket.setTcpNoDelay(true);
nextSocket.setSoTimeout(500);
nextSocket.connect(new InetSocketAddress(host, port), 10000);
if (stopped.get()) {
try {
nextSocket.close();
} catch (Exception ignored) {
}
throw new IOException("TCP reporter stopped");
}
socket = nextSocket;
inputStream = nextSocket.getInputStream();
outputStream = nextSocket.getOutputStream();
Log.i(TAG, "connected to " + host + ":" + port);
emitLog("传输", "TCP 已连接 " + host + ":" + port);
}
private void emitLog(String kind, String message) {
if (!stopped.get() && listener != null) {
listener.onReporterLog(kind, message);
}
}
private void emitTx(int orderCode, byte[] frame) {
emitLog("传输", "TX " + orderCode + " bytes=" + frame.length + " sn=" + frameSerial(frame));
emitLog("HEX", "TX " + orderCode + " " + toHex(frame, 160));
Log.i(TAG, "TX " + orderCode + " " + toHex(frame, 160));
}
private void emitRx(ResponseFrame response) {
emitLog("传输", "RX " + response.orderCode + " body=" + response.body.length + " sn=" + response.serialNum);
emitLog("HEX", "RX " + response.orderCode + " " + toHex(response.fullFrame, 160));
Log.i(TAG, "RX " + response.orderCode + " " + toHex(response.fullFrame, 160));
}
private String errorSummary(Exception error) {
if (error == null) {
return "未知错误";
}
String message = error.getMessage();
if (message == null || message.length() == 0) {
return error.getClass().getSimpleName();
}
return error.getClass().getSimpleName() + " " + message;
}
private void closeSocketQuietly() {
synchronized (socketLock) {
if (outputStream != null) {
try {
outputStream.close();
} catch (Exception ignored) {
}
}
outputStream = null;
if (inputStream != null) {
try {
inputStream.close();
} catch (Exception ignored) {
}
}
inputStream = null;
if (socket != null) {
try {
socket.close();
} catch (Exception ignored) {
}
}
socket = null;
}
}
private byte[] buildFrame(int orderCode, byte[] body) {
if (body == null) body = new byte[0];
int currentSerial = nextSerialNum();
int length = 8 + 2 + body.length + 2 + 1;
byte[] frame = new byte[HEADER.length + 2 + length];
int index = 0;
System.arraycopy(HEADER, 0, frame, index, HEADER.length);
index += HEADER.length;
putUInt16(frame, index, length);
index += 2;
putUInt64(frame, index, terminalId);
index += 8;
putUInt16(frame, index, orderCode);
index += 2;
System.arraycopy(body, 0, frame, index, body.length);
index += body.length;
putUInt16(frame, index, currentSerial);
index += 2;
frame[index] = calcCrc(frame, HEADER.length + 2, 8 + 2 + body.length + 2);
return frame;
}
private synchronized int nextSerialNum() {
int value = serialNum;
serialNum += 1;
if (serialNum > 65535) {
serialNum = 1;
}
return value;
}
private void drainResponsesLocked(long waitMs) {
if (inputStream == null) {
return;
}
long deadline = System.currentTimeMillis() + Math.max(50, waitMs);
try {
while (System.currentTimeMillis() < deadline) {
ResponseFrame response = readResponseFrame(deadline);
if (response == null) {
return;
}
emitRx(response);
}
} catch (Exception error) {
emitLog("传输", "RX 读取异常:" + errorSummary(error));
closeSocketQuietly();
}
}
private ResponseFrame readResponseFrame(long deadline) throws Exception {
if (inputStream == null) {
return null;
}
while (System.currentTimeMillis() < deadline) {
int first;
try {
first = inputStream.read();
} catch (SocketTimeoutException timeout) {
return null;
}
if (first < 0) {
return null;
}
if (first != (HEADER[0] & 0xff)) {
continue;
}
if (!readRemainingHeader()) {
continue;
}
byte[] lengthBytes = new byte[2];
readFully(lengthBytes, 0, 2);
int length = uint16(lengthBytes, 0);
if (length < 13 || length > 65535) {
continue;
}
byte[] payload = new byte[length];
readFully(payload, 0, length);
byte[] full = new byte[HEADER.length + 2 + length];
System.arraycopy(HEADER, 0, full, 0, HEADER.length);
full[HEADER.length] = lengthBytes[0];
full[HEADER.length + 1] = lengthBytes[1];
System.arraycopy(payload, 0, full, HEADER.length + 2, length);
byte expected = calcCrc(full, HEADER.length + 2, length - 1);
byte actual = payload[length - 1];
if (expected != actual) {
Log.w(TAG, "response crc mismatch expected=" + (expected & 0xff) + " actual=" + (actual & 0xff));
Log.w(TAG, "RX CRC mismatch frame=" + toHex(full, 160));
emitLog("传输", "RX CRC错误 expected=" + (expected & 0xff) + " actual=" + (actual & 0xff));
}
int orderCode = uint16(payload, 8);
int bodyLength = length - 8 - 2 - 2 - 1;
byte[] body = new byte[bodyLength];
System.arraycopy(payload, 10, body, 0, bodyLength);
int serialNum = uint16(payload, 10 + bodyLength);
return new ResponseFrame(orderCode, body, serialNum, full);
}
return null;
}
private boolean readRemainingHeader() throws Exception {
for (int i = 1; i < HEADER.length; i += 1) {
int value = inputStream.read();
if (value != (HEADER[i] & 0xff)) {
return false;
}
}
return true;
}
private void readFully(byte[] target, int offset, int length) throws Exception {
int read = 0;
while (read < length) {
int size;
try {
size = inputStream.read(target, offset + read, length - read);
} catch (SocketTimeoutException timeout) {
throw timeout;
}
if (size < 0) {
throw new java.io.IOException("socket closed");
}
read += size;
}
}
private ZhitouMemberInfo parseMemberInfo(byte[] data) {
if (data == null || data.length < 1) {
return ZhitouMemberInfo.failed("会员资料为空");
}
ZhitouMemberInfo info = new ZhitouMemberInfo();
info.verifyStatus = data[0] & 0xff;
if (info.verifyStatus == 0) {
info.error = "会员鉴权未通过";
return info;
}
if (info.verifyStatus == 2) {
info.error = "用户已被禁用";
return info;
}
if (info.verifyStatus != 1) {
info.error = "会员鉴权失败:" + info.verifyStatus;
return info;
}
if (data.length < 16) {
return ZhitouMemberInfo.failed("会员资料长度不足");
}
info.memberId = String.valueOf(uint32(data, 1));
info.memberCode = String.valueOf(uint32(data, 5));
int nameLength = data[9] & 0xff;
if (10 + nameLength + 6 > data.length) {
return ZhitouMemberInfo.failed("会员资料字段长度异常");
}
info.name = nameLength > 0 ? new String(data, 10, nameLength, UTF8) : "";
info.resiCount = data[14 + nameLength] & 0xff;
long score = uint32(data, 10 + nameLength);
info.balance = formatMoney(score);
return info;
}
private String formatMoney(long cents) {
return new BigDecimal(cents).movePointLeft(2).setScale(2).toPlainString();
}
private byte calcCrc(byte[] data, int offset, int length) {
byte crc = data[offset];
for (int i = offset + 1; i < offset + length; i += 1) {
crc ^= data[i];
}
return crc;
}
private byte[] fixedIdentityBytes(int idType, String value) {
byte[] result = new byte[33];
if (value == null) return result;
byte[] source = identityContentBytes(idType, value);
System.arraycopy(source, 0, result, 0, Math.min(source.length, result.length));
return result;
}
private int rawIdentityLength(int idType, String value) {
if (value == null) return 0;
return identityContentBytes(idType, value).length;
}
private byte[] identityContentBytes(int idType, String value) {
if (value == null) {
return new byte[0];
}
String text = value.trim();
if (idType == 1 && text.matches("(?i)^[0-9a-f]{8}$")) {
byte[] bytes = new byte[4];
for (int i = 0; i < bytes.length; i += 1) {
int start = i * 2;
bytes[i] = (byte) Integer.parseInt(text.substring(start, start + 2), 16);
}
return bytes;
}
return text.getBytes(UTF8);
}
private int kgToCentWeight(double kg) {
return clamp((int) Math.round(kg * 100), 0, 65534);
}
private int decimal4(String value) {
try {
return new BigDecimal(value).multiply(new BigDecimal(10000)).intValue();
} catch (Exception ignored) {
return 0;
}
}
private boolean hasLocation(JSONObject data) {
return decimal4(data.optString("lng", "0")) != 0
|| decimal4(data.optString("lat", "0")) != 0
|| decimal4(data.optString("altitude", "0")) != 0;
}
private long parseTerminalId(String text) {
if (text == null || text.trim().length() == 0) {
return 0;
}
try {
return Long.parseLong(text.trim());
} catch (Exception ignored) {
return 0;
}
}
private int clamp(int value, int min, int max) {
if (value < min) return min;
if (value > max) return max;
return value;
}
private int uint16(byte[] data, int offset) {
return ((data[offset] & 0xff) << 8) | (data[offset + 1] & 0xff);
}
private int frameSerial(byte[] frame) {
if (frame == null || frame.length < HEADER.length + 2 + 8 + 2 + 2 + 1) {
return 0;
}
return uint16(frame, frame.length - 3);
}
private String toHex(byte[] data, int maxBytes) {
if (data == null) {
return "";
}
int limit = Math.min(data.length, Math.max(0, maxBytes));
StringBuilder builder = new StringBuilder(limit * 3 + 16);
for (int i = 0; i < limit; i += 1) {
if (i > 0) builder.append(' ');
int value = data[i] & 0xff;
if (value < 16) builder.append('0');
builder.append(Integer.toHexString(value).toUpperCase(Locale.ROOT));
}
if (limit < data.length) {
builder.append(" ...");
}
return builder.toString();
}
private long uint32(byte[] data, int offset) {
return ((long) (data[offset] & 0xff) << 24)
| ((long) (data[offset + 1] & 0xff) << 16)
| ((long) (data[offset + 2] & 0xff) << 8)
| (long) (data[offset + 3] & 0xff);
}
private void putUInt16(byte[] data, int offset, int value) {
int v = clamp(value, 0, 65535);
data[offset] = (byte) ((v >> 8) & 0xff);
data[offset + 1] = (byte) (v & 0xff);
}
private void putUInt32(byte[] data, int offset, int value) {
data[offset] = (byte) ((value >> 24) & 0xff);
data[offset + 1] = (byte) ((value >> 16) & 0xff);
data[offset + 2] = (byte) ((value >> 8) & 0xff);
data[offset + 3] = (byte) (value & 0xff);
}
private void putUInt64(byte[] data, int offset, long value) {
data[offset] = (byte) ((value >>> 56) & 0xff);
data[offset + 1] = (byte) ((value >>> 48) & 0xff);
data[offset + 2] = (byte) ((value >>> 40) & 0xff);
data[offset + 3] = (byte) ((value >>> 32) & 0xff);
data[offset + 4] = (byte) ((value >>> 24) & 0xff);
data[offset + 5] = (byte) ((value >>> 16) & 0xff);
data[offset + 6] = (byte) ((value >>> 8) & 0xff);
data[offset + 7] = (byte) (value & 0xff);
}
private static final class ResponseFrame {
final int orderCode;
final byte[] body;
final int serialNum;
final byte[] fullFrame;
ResponseFrame(int orderCode, byte[] body, int serialNum, byte[] fullFrame) {
this.orderCode = orderCode;
this.body = body;
this.serialNum = serialNum;
this.fullFrame = fullFrame;
}
}
}
@@ -0,0 +1,54 @@
package com.xingyuan.zhiling;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.PermissionRequest;
import android.webkit.WebView;
import android.widget.Toast;
final class ZhitouWebChromeClient extends WebChromeClient {
private final MainActivity activity;
ZhitouWebChromeClient(MainActivity activity) {
this.activity = activity;
}
@Override
public void onPermissionRequest(final PermissionRequest request) {
if (request == null) {
return;
}
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
for (String resource : request.getResources()) {
if (PermissionRequest.RESOURCE_VIDEO_CAPTURE.equals(resource)) {
request.grant(new String[] { PermissionRequest.RESOURCE_VIDEO_CAPTURE });
return;
}
}
request.deny();
}
});
}
@Override
public boolean onShowFileChooser(
WebView view,
ValueCallback<Uri[]> filePath,
FileChooserParams fileChooserParams) {
activity.setFilePathCallback(filePath);
Intent intent = fileChooserParams.createIntent();
try {
activity.launchFileChooser(intent);
} catch (ActivityNotFoundException exception) {
activity.clearFilePathCallback();
Toast.makeText(activity, "无法打开文件选择器", Toast.LENGTH_SHORT).show();
return false;
}
return true;
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

+3
View File
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">星元智灵</string>
</resources>
+9
View File
@@ -0,0 +1,9 @@
<resources>
<style name="AppTheme" parent="android:style/Theme.Material.Light.NoActionBar">
<item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowFullscreen">true</item>
<item name="android:fontFamily">sans</item>
<item name="android:colorAccent">#23777F</item>
</style>
</resources>
Executable
+189
View File
@@ -0,0 +1,189 @@
#!/bin/bash
# ============================================================
# 星元智灵 APK 手动编译脚本 (无 Gradle)
# 使用 Android SDK 命令行工具: aapt / javac / d8 / zipalign / apksigner
# ============================================================
set -e
# ---- 路径配置 ----
PROJECT="$(cd "$(dirname "$0")" && pwd)"
SDK="$HOME/Library/Android/sdk"
BT="$SDK/build-tools/34.0.0"
PLATFORM="$SDK/platforms/android-28/android.jar"
SRC="$PROJECT/app/src/main"
BUILD="$PROJECT/build"
KEYSTORE="$PROJECT/keystore/zhitou-debug.keystore"
APK_BASENAME="xingyuan-zhiling"
STORE_PASS="android"
KEY_PASS="android"
KEY_ALIAS="androiddebugkey"
# ---- 工具路径 ----
AAPT="$BT/aapt"
D8="$BT/d8"
ZIPALIGN="$BT/zipalign"
APKSIGNER="$BT/apksigner"
NDK_VERSION="27.3.13750724"
NDK="$SDK/ndk/$NDK_VERSION"
ARM_CLANGXX="$NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin/armv7a-linux-androideabi21-clang++"
# ---- 输出目录 ----
OUT="$BUILD/out"
GEN="$BUILD/gen"
CLASSES="$BUILD/classes"
DEX="$BUILD/dex"
echo "=========================================="
echo " 星元智灵 APK 编译"
echo "=========================================="
echo "SDK: $SDK"
echo "BuildTools: 34.0.0"
echo "Platform: android-28"
echo "Project: $PROJECT"
echo ""
# ---- Debug 签名证书 ----
if [ ! -f "$KEYSTORE" ]; then
echo "未找到 debug keystore,正在生成: $KEYSTORE"
mkdir -p "$(dirname "$KEYSTORE")"
keytool -genkeypair \
-keystore "$KEYSTORE" \
-storepass "$STORE_PASS" \
-keypass "$KEY_PASS" \
-alias "$KEY_ALIAS" \
-keyalg RSA \
-keysize 2048 \
-validity 10000 \
-dname "CN=Android Debug,O=Xingyuan Zhiling,C=CN" >/dev/null
fi
# ---- Step 1: 清理 ----
echo "[1/8] 清理旧的编译产物..."
rm -rf "$GEN" "$CLASSES" "$DEX"
mkdir -p "$GEN" "$CLASSES" "$DEX" "$OUT"
# ---- Step 2: 生成 R.java ----
echo "[2/8] 生成 R.java..."
"$AAPT" package -f -m \
-J "$GEN" \
-M "$SRC/AndroidManifest.xml" \
-S "$SRC/res" \
-A "$SRC/assets" \
-I "$PLATFORM"
# ---- Step 3: 收集 Java 源文件 ----
echo "[3/8] 收集 Java 源文件..."
SRC_FILES=$(find "$SRC/java" "$GEN" -name "*.java" | tr '\n' ' ')
echo " 源文件数量: $(echo $SRC_FILES | wc -w | tr -d ' ')"
# ---- Step 4: 编译 Java ----
echo "[4/8] 编译 Java 源码..."
javac --release 8 -parameters \
-classpath "$PLATFORM" \
-sourcepath "$GEN" \
-d "$CLASSES" \
$SRC_FILES 2>&1
echo " 编译完成"
# ---- Step 5: 转换为 DEX ----
echo "[5/8] 转换为 DEX..."
CLASS_FILES=$(find "$CLASSES" -name "*.class" | tr '\n' ' ')
"$D8" \
--output "$DEX" \
--lib "$PLATFORM" \
$CLASS_FILES 2>&1
echo " DEX 生成完成: $DEX/classes.dex"
# ---- Step 6: 打包资源 + Manifest + Assets ----
echo "[6/8] 打包 APK (资源 + Manifest + Assets)..."
rm -f "$OUT/$APK_BASENAME-unsigned.apk" "$OUT/$APK_BASENAME-aligned.apk" "$OUT/$APK_BASENAME-signed.apk"
"$AAPT" package -f \
-M "$SRC/AndroidManifest.xml" \
-S "$SRC/res" \
-A "$SRC/assets" \
-I "$PLATFORM" \
-F "$OUT/$APK_BASENAME-unsigned.apk"
# ---- Step 7: 添加 DEX 和 Native 库 ----
echo "[7/8] 添加 DEX 和 Native 库到 APK..."
# 添加 classes.dex
cd "$DEX"
zip -j "$OUT/$APK_BASENAME-unsigned.apk" classes.dex
cd "$PROJECT"
# 构建 SeetaFace6 JNI 桥(官方 Android 库就位后自动启用)
FACE_LIB_DIR="$SRC/jniLibs/armeabi-v7a"
FACE_JNI_SOURCE="$SRC/cpp/face_engine_jni.cpp"
FACE_JNI_OUTPUT="$FACE_LIB_DIR/libxingyuan_face.so"
FACE_REQUIRED_LIBS=(
libTenniS.so
libSeetaAuthorize.so
libSeetaFaceDetector600.so
libSeetaFaceLandmarker600.so
libSeetaFaceRecognizer600.so
)
FACE_LIBS_READY=true
for library in "${FACE_REQUIRED_LIBS[@]}"; do
if [ ! -f "$FACE_LIB_DIR/$library" ]; then
FACE_LIBS_READY=false
fi
done
if [ "$FACE_LIBS_READY" = true ] && [ -x "$ARM_CLANGXX" ]; then
echo " 编译 SeetaFace6 JNI 桥..."
"$ARM_CLANGXX" \
--sysroot="$NDK/toolchains/llvm/prebuilt/darwin-x86_64/sysroot" \
-std=c++11 -O2 -fPIC -shared -static-libstdc++ \
-I"$SRC/cpp/include" \
"$FACE_JNI_SOURCE" \
-L"$FACE_LIB_DIR" \
-lSeetaFaceDetector600 \
-lSeetaFaceLandmarker600 \
-lSeetaFaceRecognizer600 \
-lSeetaAuthorize \
-lTenniS \
-llog -lz -ldl -lm \
-o "$FACE_JNI_OUTPUT"
else
rm -f "$FACE_JNI_OUTPUT"
echo " ⚠ SeetaFace6 官方 Android 动态库尚未齐备,人脸 JNI 暂不打包"
fi
# 添加 native 库 (APK 中需要 lib/<abi>/ 结构)
NATIVE_STAGING="$BUILD/native_staging"
rm -rf "$NATIVE_STAGING"
mkdir -p "$NATIVE_STAGING/lib/armeabi-v7a"
find "$SRC/jniLibs/armeabi-v7a" -maxdepth 1 -type f -name "*.so" -exec cp {} "$NATIVE_STAGING/lib/armeabi-v7a/" \;
cd "$NATIVE_STAGING"
zip -r "$OUT/$APK_BASENAME-unsigned.apk" lib/
cd "$PROJECT"
echo " APK 打包完成"
# ---- Step 8: Zipalign + 签名 ----
echo "[8/8] Zipalign 并签名..."
# Zipalign
"$ZIPALIGN" -f 4 "$OUT/$APK_BASENAME-unsigned.apk" "$OUT/$APK_BASENAME-aligned.apk"
# 签名
"$APKSIGNER" sign \
--ks "$KEYSTORE" \
--ks-pass "pass:$STORE_PASS" \
--ks-key-alias "$KEY_ALIAS" \
--key-pass "pass:$KEY_PASS" \
--out "$OUT/$APK_BASENAME-signed.apk" \
"$OUT/$APK_BASENAME-aligned.apk"
# 验证签名
"$APKSIGNER" verify --verbose "$OUT/$APK_BASENAME-signed.apk" 2>&1
# ---- 完成 ----
echo ""
echo "=========================================="
echo " ✅ 编译成功!"
echo "=========================================="
echo ""
ls -lh "$OUT/$APK_BASENAME-signed.apk"
echo ""
echo "输出文件: $OUT/$APK_BASENAME-signed.apk"
echo ""
echo "安装命令:"
echo " adb install -r $OUT/$APK_BASENAME-signed.apk"
+87
View File
@@ -0,0 +1,87 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>星元智灵异常场景测试</title>
<link rel="stylesheet" href="../../app/src/main/assets/www/styles.css" />
</head>
<body>
<main id="app" class="app-shell" aria-live="polite"></main>
<div id="toast" class="toast" role="status"></div>
<script>
(function () {
var params = new URLSearchParams(location.search);
var scenario = params.get("scenario") || "pass";
var baselineDelay = Number(params.get("baselineDelay") || 30);
var weightQueryCounts = {};
if (params.get("clear") === "1") localStorage.clear();
window.__nativeCalls = [];
function record(method, json) {
var payload = {};
try { payload = JSON.parse(json || "{}"); } catch (error) {}
window.__nativeCalls.push({ method: method, payload: payload, at: Date.now() });
document.documentElement.setAttribute("data-native-calls", JSON.stringify(window.__nativeCalls));
return payload;
}
function emitConnection(payload, delay) {
window.setTimeout(function () {
if (window.ZhitouSerial) window.ZhitouSerial.onConnectionCheck(payload);
}, delay || 20);
}
window.XingyuanNative = {
configure: function (json) { record("configure", json); },
startHeartbeat: function (json) { record("startHeartbeat", json); },
scanMediaFiles: function () { return "[]"; },
loadLocalBackups: function () { return "[]"; },
fetchWorkContext: function () {
return JSON.stringify([{ machineNo: "TEST-01", persons: ["测试员"], batches: ["BATCH-01"] }]);
},
requestConnectionCheck: function (json) {
var request = record("requestConnectionCheck", json);
var serialOnline = scenario !== "serial-fail" && scenario !== "all-fail";
var networkOnline = scenario !== "network-fail" && scenario !== "all-fail";
emitConnection({
requestId: request.requestId,
mode: request.mode,
serialOnline: serialOnline,
networkOnline: networkOnline,
tcpConnected: networkOnline,
httpReachable: networkOnline,
pingOk: networkOnline
}, scenario === "slow-check" ? 4300 : 20);
},
queryWeight: function (json) {
var request = record("queryWeight", json);
var port = Number(request.port || 1);
weightQueryCounts[port] = Number(weightQueryCounts[port] || 0) + 1;
if (scenario !== "weight-missing" && !(scenario === "baseline-missing" && weightQueryCounts[port] === 1)) {
window.setTimeout(function () {
if (window.ZhitouSerial) {
window.ZhitouSerial.onThrowWeight({
port: port,
weight: 5,
beforeWeight: 5,
afterWeight: 5,
queryResponse: true
});
}
}, baselineDelay);
}
return true;
},
queryFill: function (json) { record("queryFill", json); return true; },
queryTemperature: function (json) { record("queryTemperature", json); return true; },
openThrowDoor: function (json) { record("openThrowDoor", json); return scenario !== "open-fail"; },
closeThrowDoor: function (json) { record("closeThrowDoor", json); return scenario !== "close-fail"; },
reportXingyuanDelivery: function (json) { record("reportXingyuanDelivery", json); return true; },
saveLocalBackup: function (json) { record("saveLocalBackup", json); return "/tmp/test-backup.json"; }
};
})();
</script>
<script src="../../app/src/main/assets/www/app.js"></script>
</body>
</html>