feat: 初始化星元智灵人脸识别回收箱项目
@@ -0,0 +1,55 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.xingyuan.zhiling"
|
||||
android:versionCode="107"
|
||||
android:versionName="1.2.18-wal-stability-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>
|
||||
|
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.
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -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>
|
||||
@@ -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,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;
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 55 KiB |
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">星元智灵</string>
|
||||
</resources>
|
||||
@@ -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>
|
||||