diff --git a/.gitignore b/.gitignore
index ecb1d3c..70c4e50 100644
--- a/.gitignore
+++ b/.gitignore
@@ -88,3 +88,4 @@ lint/tmp/
/app/src/androidTest/java/com/jiaoguanyi/appstore/
/adb
/app/proguard-rules.pro
+/.codebuddy/
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index b890c74..5fe092b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -29,7 +29,7 @@ android {
ndk {
//选择要添加的对应 cpu 类型的 .so 库。
- abiFilters 'arm64-v8a'
+ abiFilters 'arm64-v8a', 'armeabi-v7a'
// 还可以添加 'armeabi', 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64', 'mips', 'mips64'
}
@@ -736,13 +736,25 @@ dependencies {
implementation files('libs/BaiduTraceSDK_v3_1_10.jar')
+ // 基础注解库
+ implementation "androidx.annotation:annotation:1.10.0"
+ // 包含 OptIn 实验性 Lint 规则检测的核心库
+ implementation "androidx.annotation:annotation-experimental:1.1.0"
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
-
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
+ implementation "androidx.lifecycle:lifecycle-service:2.3.1"
+
+ // CameraX 核心库及生命周期绑定
+ def camerax_version = "1.0.0" // 请根据实际情况选择稳定版本
+ implementation "androidx.camera:camera-core:${camerax_version}"
+ implementation "androidx.camera:camera-camera2:${camerax_version}"
+ implementation "androidx.camera:camera-lifecycle:${camerax_version}"
+ implementation "androidx.camera:camera-view:1.0.0-alpha23"
+
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
@@ -776,7 +788,8 @@ dependencies {
//Google
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'com.google.zxing:core:3.5.0'
-// implementation 'com.google.mlkit:face-detection:16.1.7'
+ implementation 'com.google.mlkit:face-detection:16.1.5'
+
//图片加载框架
implementation 'com.github.bumptech.glide:glide:4.13.2'
annotationProcessor 'com.github.bumptech.glide:compiler:4.13.2'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index baee31b..23338b0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -11,6 +11,10 @@
+
+
+
+
@@ -36,7 +40,6 @@
-
@@ -92,6 +95,8 @@
+
+
@@ -168,6 +173,7 @@
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/AppTheme">
+
@@ -183,7 +189,6 @@
android:icon="@drawable/com_system_huyan"
android:label="护眼助手"
android:launchMode="singleInstance"
- android:screenOrientation="userLandscape"
android:theme="@style/AppTheme">
@@ -233,7 +238,26 @@
android:launchMode="singleTask"
android:screenOrientation="behind"
android:theme="@style/DialogCloseOnTouchOutside" />
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/aoleyun/sn/activity/CameraDetectionActivity.java b/app/src/main/java/com/aoleyun/sn/activity/CameraDetectionActivity.java
new file mode 100644
index 0000000..5b27512
--- /dev/null
+++ b/app/src/main/java/com/aoleyun/sn/activity/CameraDetectionActivity.java
@@ -0,0 +1,253 @@
+package com.aoleyun.sn.activity;
+
+import android.Manifest;
+import android.content.pm.PackageManager;
+import android.graphics.PointF;
+import android.media.Image;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.ExperimentalGetImage;
+import androidx.camera.core.ImageAnalysis;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.Preview;
+import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.camera.view.PreviewView;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+
+import com.aoleyun.sn.R;
+import com.aoleyun.sn.detection.DistanceReminderManager;
+import com.aoleyun.sn.detection.ShakeDetectionManager;
+import com.aoleyun.sn.view.FaceOverlayView;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.mlkit.vision.common.InputImage;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class CameraDetectionActivity extends AppCompatActivity {
+
+ private static final String TAG = "CameraInputImage";
+ private static final int PERMISSION_REQUEST_CAMERA = 1001;
+
+ private ListenableFuture cameraProviderFuture;
+ private ExecutorService cameraExecutor;
+ private PreviewView previewView;
+ private FaceOverlayView faceOverlay;
+ private android.widget.TextView tvStepInfo;
+ private int currentImageWidth;
+ private int currentImageHeight;
+
+ public static boolean isShowing = false;
+
+ private DistanceReminderManager reminderManager;
+ private ShakeDetectionManager shakeDetectionManager;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // 如果你需要预览界面,可以使用 xml 里的 PreviewView,这里仅演示后台获取数据流
+ setContentView(R.layout.activity_camera_detection);
+ previewView = findViewById(R.id.previewView);
+ faceOverlay = findViewById(R.id.faceOverlay);
+ tvStepInfo = findViewById(R.id.tvStepInfo);
+
+ // 初始化用于处理图像分析的单线程线程池,避免阻塞主线程
+ cameraExecutor = Executors.newSingleThreadExecutor();
+
+ // 检查并请求相机权限
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
+ == PackageManager.PERMISSION_GRANTED) {
+ startCamera();
+ } else {
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.CAMERA}, PERMISSION_REQUEST_CAMERA);
+ }
+
+ reminderManager = new DistanceReminderManager(new DistanceReminderManager.OnDistanceAlertListener() {
+ @Override
+ public void onTooClose() {
+ // 触发提醒:弹出 Activity、播放提示音或让屏幕变模糊
+ WarningDialogActivity.start(CameraDetectionActivity.this, WarningDialogActivity.TYPE_DISTANCE);
+ }
+
+ @Override
+ public void onDistanceUpdate(double distance, PointF leftEye, PointF rightEye) {
+ // 传给 Overlay 进行比例换算
+ faceOverlay.updateData(distance, leftEye, rightEye, currentImageWidth, currentImageHeight);
+ }
+ });
+
+ // 初始化抖动(走路)检测
+ shakeDetectionManager = new ShakeDetectionManager(this, new ShakeDetectionManager.OnShakeListener() {
+ @Override
+ public void onShakeDetected() {
+ // 边走路边看平板提醒
+ WarningDialogActivity.start(CameraDetectionActivity.this, WarningDialogActivity.TYPE_SHAKE);
+ }
+
+ @Override
+ public void onStatusUpdate(String info) {
+ runOnUiThread(() -> {
+ if (tvStepInfo != null) {
+ tvStepInfo.setText(info);
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * 初始化并启动 CameraX
+ */
+ private void startCamera() {
+ cameraProviderFuture = ProcessCameraProvider.getInstance(this);
+ cameraProviderFuture.addListener(() -> {
+ try {
+ ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
+ bindCameraUseCases(cameraProvider);
+ } catch (ExecutionException | InterruptedException e) {
+ Log.e(TAG, "初始化 CameraProvider 失败", e);
+ }
+ }, ContextCompat.getMainExecutor(this));
+ }
+
+ /**
+ * 配置并绑定相机用例
+ */
+ private void bindCameraUseCases(@NonNull ProcessCameraProvider cameraProvider) {
+ // 1. 选择【前置摄像头】
+ CameraSelector cameraSelector = new CameraSelector.Builder()
+ .requireLensFacing(CameraSelector.LENS_FACING_FRONT)
+ .build();
+
+ // 2. 创建预览用例
+ Preview preview = new Preview.Builder().build();
+ preview.setSurfaceProvider(previewView.getSurfaceProvider());
+
+ // 3. 创建图像分析用例(ImageAnalysis)
+ ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
+ // 设置背压策略:如果图像处理太慢,丢弃旧帧,保留最新的一帧(防积压爆内存)
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
+ // 建议设置输出的目标分辨率(例如 480x640 或 720x1280),对于人脸/距离检测绰绰有余,且能提升运行速度
+ .setTargetResolution(new android.util.Size(640, 480))
+ .build();
+
+ // 3. 设置分析器,在这里把每一帧转换为 InputImage
+ imageAnalysis.setAnalyzer(cameraExecutor, new ImageAnalysis.Analyzer() {
+ private long lastAnalyzedTimestamp = 0L;
+
+ @OptIn(markerClass = ExperimentalGetImage.class)
+ @Override
+ public void analyze(@NonNull ImageProxy imageProxy) {
+ // 【性能调优】如果是防近视提醒,不需要一秒分析30次。
+ // 这里限制为每 1000 毫秒(1秒)才执行一次算法
+ long currentTimestamp = System.currentTimeMillis();
+ if (currentTimestamp - lastAnalyzedTimestamp < 1000) {
+ imageProxy.close(); // 必须关闭,否则不会收到下一帧
+ return;
+ }
+ lastAnalyzedTimestamp = currentTimestamp;
+
+ // 从 imageProxy 中提取原生的 android.media.Image
+ Image mediaImage = imageProxy.getImage();
+ if (mediaImage != null) {
+ // 获取图像的旋转角度(前置摄像头和设备方向有关,ML Kit 需要这个角度来正确旋转识别)
+ int rotationDegrees = imageProxy.getImageInfo().getRotationDegrees();
+
+ // 【核心代码】将原生 Image 转换为 ML Kit 的 InputImage
+ InputImage inputImage = InputImage.fromMediaImage(mediaImage, rotationDegrees);
+
+ // 此时已经成功拿到 InputImage,可以传给你的算法或 ML Kit 识别器
+ // 【优化】传入 imageProxy 以便在异步任务完成后关闭它
+ processInputImage(inputImage, imageProxy);
+ } else {
+ // 如果获取不到 Image,也需要关闭 imageProxy
+ imageProxy.close();
+ }
+ }
+ });
+
+ try {
+ // 在绑定前先解绑所有用例
+ cameraProvider.unbindAll();
+
+ // 将前置摄像头、预览和图像分析用例绑定到当前 Activity 的生命周期
+ cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalysis);
+
+ } catch (Exception e) {
+ Log.e(TAG, "绑定相机用例失败", e);
+ }
+ }
+
+ /**
+ * 业务处理方法
+ */
+ private void processInputImage(InputImage inputImage, ImageProxy imageProxy) {
+ // 在这里调用你的人脸检测或者瞳距计算算法
+ // Log.d(TAG, "成功获取到 InputImage,尺寸: " + inputImage.getWidth() + "x" + inputImage.getHeight());
+
+ // 记录当前图像尺寸,供 UI 绘制缩放使用
+ // 重要:InputImage 的 getWidth/Height 返回的是原始 buffer 尺寸,
+ // 而 ML Kit 的坐标是基于旋转后的。如果是 90/270 度旋转,宽高需要对调。
+ int rotation = imageProxy.getImageInfo().getRotationDegrees();
+ if (rotation == 90 || rotation == 270) {
+ currentImageWidth = inputImage.getHeight();
+ currentImageHeight = inputImage.getWidth();
+ } else {
+ currentImageWidth = inputImage.getWidth();
+ currentImageHeight = inputImage.getHeight();
+ }
+
+ // 比如传入上一问提到的人脸识别:
+ // 【关键修复】在 ML Kit 任务完成后再关闭 imageProxy,避免 Internal error (ModelResource.zza)
+ reminderManager.checkDistance(inputImage)
+ .addOnCompleteListener(task -> imageProxy.close());
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ isShowing = true;
+ if (shakeDetectionManager != null) {
+ shakeDetectionManager.start();
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ isShowing = false;
+ if (shakeDetectionManager != null) {
+ shakeDetectionManager.stop();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 关闭线程池,防止内存泄漏
+ if (cameraExecutor != null) {
+ cameraExecutor.shutdown();
+ }
+ }
+
+ // 处理权限回调
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ if (requestCode == PERMISSION_REQUEST_CAMERA) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ startCamera();
+ } else {
+ WarningDialogActivity.start(this, WarningDialogActivity.TYPE_DISTANCE, "需要相机权限来检测距离");
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/aoleyun/sn/activity/EyeProtectionActivity.java b/app/src/main/java/com/aoleyun/sn/activity/EyeProtectionActivity.java
index 95a348b..31fd6d2 100644
--- a/app/src/main/java/com/aoleyun/sn/activity/EyeProtectionActivity.java
+++ b/app/src/main/java/com/aoleyun/sn/activity/EyeProtectionActivity.java
@@ -1,20 +1,28 @@
package com.aoleyun.sn.activity;
+import android.content.Intent;
import android.provider.Settings;
import android.view.View;
-import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.databinding.DataBindingUtil;
+import com.aoleyun.sn.BuildConfig;
import com.aoleyun.sn.R;
import com.aoleyun.sn.base.BaseDataBindingActivity;
-import com.aoleyun.sn.base.mvp.BaseMvpActivity;
+import com.aoleyun.sn.comm.CommonConfig;
import com.aoleyun.sn.databinding.ActivityEyeProtectionBinding;
+import com.aoleyun.sn.detection.SensorCheckUtil;
+import com.aoleyun.sn.service.DetectionService;
+import com.aoleyun.sn.service.ShakeDetectionService;
import com.aoleyun.sn.view.ToggleButton;
import com.hjq.toast.Toaster;
+import com.tencent.mmkv.MMKV;
public class EyeProtectionActivity extends BaseDataBindingActivity {
+ private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE);
+
+
private ActivityEyeProtectionBinding mBinding;
@@ -26,6 +34,7 @@ public class EyeProtectionActivity extends BaseDataBindingActivity {
@Override
protected void initDataBinding() {
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_eye_protection);
+ mBinding.setClick(new BtnClick());
}
@Override
@@ -65,6 +74,30 @@ public class EyeProtectionActivity extends BaseDataBindingActivity {
}
});
+ mBinding.toggleButton6.setOnToggleChanged(new ToggleButton.OnToggleChanged() {
+ @Override
+ public void onToggle(boolean on) {
+ mMMKV.encode(CommonConfig.SHAKE_REMINDER, on ? 1 : 0);
+ if (on) {
+ startService(new Intent(EyeProtectionActivity.this, ShakeDetectionService.class));
+ } else {
+ stopService(new Intent(EyeProtectionActivity.this, ShakeDetectionService.class));
+ }
+ }
+ });
+
+ mBinding.toggleButton7.setOnToggleChanged(new ToggleButton.OnToggleChanged() {
+ @Override
+ public void onToggle(boolean on) {
+ mMMKV.encode(CommonConfig.EYE_TO_SCREEN_DISTANCE_REMINDER, on ? 1 : 0);
+ if (on) {
+ startService(new Intent(EyeProtectionActivity.this, DetectionService.class));
+ } else {
+ stopService(new Intent(EyeProtectionActivity.this, DetectionService.class));
+ }
+ }
+ });
+
mBinding.clExit.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
@@ -94,5 +127,29 @@ public class EyeProtectionActivity extends BaseDataBindingActivity {
} else {
mBinding.toggleButton2.setToggleOff();
}
+
+ int shakeReminder = mMMKV.decodeInt(CommonConfig.SHAKE_REMINDER, 0);
+ if (shakeReminder == 1) {
+ mBinding.toggleButton6.setToggleOn();
+ } else {
+ mBinding.toggleButton6.setToggleOff();
+ }
+
+ int reminder = mMMKV.decodeInt(CommonConfig.EYE_TO_SCREEN_DISTANCE_REMINDER, 0);
+ if (reminder == 1) {
+ mBinding.toggleButton7.setToggleOn();
+ } else {
+ mBinding.toggleButton7.setToggleOff();
+ }
+ }
+
+ public class BtnClick {
+ public void onDistanceClick(View view) {
+ if (BuildConfig.DEBUG) {
+ if (!SensorCheckUtil.hasProximitySensor(EyeProtectionActivity.this)) {
+ startActivity(new Intent(EyeProtectionActivity.this, CameraDetectionActivity.class));
+ }
+ }
+ }
}
}
diff --git a/app/src/main/java/com/aoleyun/sn/activity/WarningDialogActivity.java b/app/src/main/java/com/aoleyun/sn/activity/WarningDialogActivity.java
new file mode 100644
index 0000000..eee3e12
--- /dev/null
+++ b/app/src/main/java/com/aoleyun/sn/activity/WarningDialogActivity.java
@@ -0,0 +1,85 @@
+package com.aoleyun.sn.activity;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.aoleyun.sn.R;
+
+public class WarningDialogActivity extends AppCompatActivity {
+
+ public static final String EXTRA_TYPE = "extra_type";
+ public static final String EXTRA_MESSAGE = "extra_message";
+ public static final int TYPE_DISTANCE = 0;
+ public static final int TYPE_SHAKE = 1;
+
+ private final Handler handler = new Handler(Looper.getMainLooper());
+ private final Runnable finishRunnable = this::finish;
+
+ public static void start(Context context, int type) {
+ start(context, type, null);
+ }
+
+ public static void start(Context context, int type, String message) {
+ Intent intent = new Intent(context, WarningDialogActivity.class);
+ intent.putExtra(EXTRA_TYPE, type);
+ if (message != null) {
+ intent.putExtra(EXTRA_MESSAGE, message);
+ }
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ context.startActivity(intent);
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_warning_dialog);
+
+ findViewById(R.id.iv_close).setOnClickListener(v -> finish());
+ findViewById(R.id.btn_confirm).setOnClickListener(v -> finish());
+
+ updateUI(getIntent());
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ setIntent(intent);
+ updateUI(intent);
+ }
+
+ private void updateUI(Intent intent) {
+ int type = intent.getIntExtra(EXTRA_TYPE, TYPE_DISTANCE);
+ String customMessage = intent.getStringExtra(EXTRA_MESSAGE);
+
+ ImageView ivIcon = findViewById(R.id.iv_warning_icon);
+ TextView tvTitle = findViewById(R.id.tv_title);
+ TextView tvMessage = findViewById(R.id.tv_message);
+
+ if (type == TYPE_SHAKE) {
+ ivIcon.setImageResource(R.drawable.ic_warning_shake);
+ tvTitle.setText("检测到抖动");
+ tvMessage.setText(customMessage != null ? customMessage : "设备正在抖动,请确认是否为误触");
+ } else {
+ ivIcon.setImageResource(R.drawable.ic_warning);
+ tvTitle.setText("距离太近");
+ tvMessage.setText(customMessage != null ? customMessage : "请保持安全距离,保护您的视力");
+ }
+
+ handler.removeCallbacks(finishRunnable);
+ handler.postDelayed(finishRunnable, 3000);
+ }
+
+ @Override
+ public void finish() {
+ super.finish();
+ overridePendingTransition(0, android.R.anim.fade_out);
+ }
+}
diff --git a/app/src/main/java/com/aoleyun/sn/activity/main/MainViewModel.java b/app/src/main/java/com/aoleyun/sn/activity/main/MainViewModel.java
index 05e82e5..60faea9 100644
--- a/app/src/main/java/com/aoleyun/sn/activity/main/MainViewModel.java
+++ b/app/src/main/java/com/aoleyun/sn/activity/main/MainViewModel.java
@@ -198,17 +198,13 @@ public class MainViewModel extends BaseViewModel> checkDistance(InputImage image) {
+ return detector.process(image)
+ .addOnSuccessListener(faces -> {
+ boolean faceFound = false;
+ for (Face face : faces) {
+ // 获取左眼和右眼的关键点
+ FaceLandmark leftEye = face.getLandmark(FaceLandmark.LEFT_EYE);
+ FaceLandmark rightEye = face.getLandmark(FaceLandmark.RIGHT_EYE);
+
+ if (leftEye != null && rightEye != null) {
+ faceFound = true;
+ PointF leftEyePos = leftEye.getPosition();
+ PointF rightEyePos = rightEye.getPosition();
+
+ // 计算两眼之间的像素距离 (勾股定理)
+ double eyeDistance = Math.sqrt(
+ Math.pow(rightEyePos.x - leftEyePos.x, 2) +
+ Math.pow(rightEyePos.y - leftEyePos.y, 2)
+ );
+
+ Log.e(TAG, "checkDistance: 当前瞳距像素值: " + eyeDistance + ", 图片分辨率: " + image.getWidth() + "x" + image.getHeight());
+
+ if (listener != null) {
+ listener.onDistanceUpdate(eyeDistance, leftEyePos, rightEyePos);
+ }
+
+ // 计算归一化距离。
+ // 【优化】为了解决横屏判断距离比竖屏短的问题,我们使用图像高度(image.getHeight())作为基准。
+ // 在大多数传感器上,横屏时的高度 FOV 与竖屏时的宽度像素比例更接近,
+ // 使用高度作为分母可以有效补偿横竖屏切换时的像素密度差异,使判断距离趋于一致。
+ double normalizedDistance = eyeDistance / image.getHeight();
+
+ Log.e(TAG, "checkDistance: 归一化距离 (基于高度): " + normalizedDistance + ", 阈值: " + TOO_CLOSE_RATIO_THRESHOLD);
+
+ // 如果归一化距离大于阈值,说明离得太近了
+ if (normalizedDistance > TOO_CLOSE_RATIO_THRESHOLD) {
+ if (listener != null) {
+ listener.onTooClose();
+ }
+ }
+ }
+ }
+ if (!faceFound && listener != null) {
+ listener.onDistanceUpdate(0, null, null);
+ }
+ })
+ .addOnFailureListener(e -> Log.e(TAG, "人脸检测失败", e));
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aoleyun/sn/detection/SensorCheckUtil.java b/app/src/main/java/com/aoleyun/sn/detection/SensorCheckUtil.java
new file mode 100644
index 0000000..8af703f
--- /dev/null
+++ b/app/src/main/java/com/aoleyun/sn/detection/SensorCheckUtil.java
@@ -0,0 +1,42 @@
+package com.aoleyun.sn.detection;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorManager;
+import android.util.Log;
+
+public class SensorCheckUtil {
+
+ /**
+ * 检测设备是否有距离感应器
+ *
+ * @param context 上下文
+ * @return true 有距离感应器;false 没有
+ */
+ public static boolean hasProximitySensor(Context context) {
+ // 1. 获取系统的传感器管理器
+ SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
+
+ if (sensorManager == null) {
+ return false;
+ }
+
+ // 2. 获取默认的距离传感器
+ Sensor proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
+
+ // 3. 判断是否为空
+ if (proximitySensor != null) {
+ Log.e("SensorCheck", "找到距离感应器: " + proximitySensor.getName());
+
+ String name = proximitySensor.getName(); // 传感器名称
+ String vendor = proximitySensor.getVendor(); // 厂商
+ float maxRange = proximitySensor.getMaximumRange(); // 最大测量距离(通常厘米为单位,很多只有 0.0 或 5.0)
+
+ Log.e("SensorDetails", "名称: " + name + ", 厂家: " + vendor + ", 最大范围: " + maxRange);
+ return true;
+ } else {
+ Log.e("SensorCheck", "此设备没有距离感应器硬件");
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aoleyun/sn/detection/ShakeDetectionManager.java b/app/src/main/java/com/aoleyun/sn/detection/ShakeDetectionManager.java
new file mode 100644
index 0000000..f85aefc
--- /dev/null
+++ b/app/src/main/java/com/aoleyun/sn/detection/ShakeDetectionManager.java
@@ -0,0 +1,227 @@
+package com.aoleyun.sn.detection;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Build;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+
+/**
+ * 抖动检测管理类(用于实现边走路边看平板的护眼提醒)
+ * 优先使用系统步数检测器(Step Detector),若硬件不支持则自动回退到线性加速度方差算法。
+ */
+public class ShakeDetectionManager implements SensorEventListener {
+ private static final String TAG = "ShakeDetection";
+ private SensorManager sensorManager;
+ private Sensor stepSensor;
+ private Sensor accelSensor;
+ private OnShakeListener listener;
+
+ // --- 方案一:步数检测相关参数 ---
+ private final List stepTimestamps = new ArrayList<>();
+ // 判定为“正在走路”的阈值:5秒内走了 4 步以上
+ private static final long STEP_TIME_WINDOW = 5000;
+ private static final int STEP_THRESHOLD = 4;
+
+ // --- 方案二:加速度方差相关参数 ---
+ private final Queue magnitudeWindow = new LinkedList<>();
+ // 采样窗口大小:1.5 秒大约需要 25 个样本
+ private static final int VARIANCE_WINDOW_SIZE = 25;
+ // 提高方差阈值:正常的拿起放下抖动通常在 1.0~1.8 之间,我们将阈值提高到 2.0
+ private static final float VARIANCE_THRESHOLD = 2.0f;
+ // 增加评估频率控制:每 400ms 评估一次方差
+ private long lastVarianceEvalTime = 0;
+ private static final long VARIANCE_EVAL_INTERVAL = 400;
+ // 持续检测到抖动的评估次数:连续 4 次评估达标才判定(400ms * 4 = 1.6秒的持续抖动)
+ private int consecutiveShakeCount = 0;
+ private static final int CONSECUTIVE_THRESHOLD = 4;
+
+ public interface OnShakeListener {
+ /**
+ * 当检测到持续抖动(疑似走路)时回调
+ */
+ void onShakeDetected();
+
+ /**
+ * 实时步数或抖动强度更新
+ *
+ * @param info 描述信息(如 "步数: 3" 或 "抖动方差: 1.5")
+ */
+ default void onStatusUpdate(String info) {
+ }
+ }
+
+ public ShakeDetectionManager(Context context, OnShakeListener listener) {
+ this.listener = listener;
+ sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
+ if (sensorManager != null) {
+ // 1. 尝试获取步数检测传感器 (最精准、最省电)
+ stepSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_DETECTOR);
+ if (stepSensor != null) {
+ Log.d(TAG, "检测到硬件步数检测器: " + stepSensor.getName());
+ } else {
+ Log.d(TAG, "无硬件步数检测器: ");
+ }
+
+ // 2. 获取线性加速度计作为备选 (排除重力干扰)
+ accelSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION);
+ if (accelSensor != null) {
+ Log.d(TAG, "检测到线性加速度计: " + accelSensor.getName());
+ } else {
+ // 如果没有线性加速度计,降级使用普通加速度计
+ accelSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+ if (accelSensor != null) {
+ Log.d(TAG, "未检测到线性加速度计,降级使用普通加速度计: " + accelSensor.getName());
+ }
+ }
+ }
+
+ if (stepSensor == null && accelSensor == null) {
+ Log.e(TAG, "错误:该设备不支持任何步数或加速度传感器,抖动检测功能将失效。");
+ }
+ }
+
+ public void start() {
+ if (sensorManager == null) return;
+
+ // 优先使用步数检测
+ if (stepSensor != null) {
+ Log.i(TAG, "启动检测:优先模式 [步数检测器]");
+ sensorManager.registerListener(this, stepSensor, SensorManager.SENSOR_DELAY_NORMAL);
+ } else if (accelSensor != null) {
+ Log.i(TAG, "启动检测:回退模式 [加速度方差算法]");
+ sensorManager.registerListener(this, accelSensor, SensorManager.SENSOR_DELAY_UI);
+ }
+ }
+
+ public void stop() {
+ Log.d(TAG, "停止检测,重置状态。");
+ if (sensorManager != null) {
+ sensorManager.unregisterListener(this);
+ }
+ stepTimestamps.clear();
+ magnitudeWindow.clear();
+ consecutiveShakeCount = 0;
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ int sensorType = event.sensor.getType();
+
+ if (sensorType == Sensor.TYPE_STEP_DETECTOR) {
+ handleStepDetection();
+ } else if (sensorType == Sensor.TYPE_LINEAR_ACCELERATION || sensorType == Sensor.TYPE_ACCELEROMETER) {
+ handleAccelDetection(event);
+ }
+ }
+
+ /**
+ * 方案一:基于步数检测器的算法
+ */
+ private void handleStepDetection() {
+ long currentTime = System.currentTimeMillis();
+ stepTimestamps.add(currentTime);
+
+ // 清除超出时间窗口的历史数据
+ long windowStartTime = currentTime - STEP_TIME_WINDOW;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ stepTimestamps.removeIf(timestamp -> timestamp < windowStartTime);
+ } else {
+ // 兼容低版本
+ while (!stepTimestamps.isEmpty() && stepTimestamps.get(0) < windowStartTime) {
+ stepTimestamps.remove(0);
+ }
+ }
+
+ Log.v(TAG, "Step Detector: 检测到步伐,当前窗口步数=" + stepTimestamps.size());
+ if (listener != null) {
+ listener.onStatusUpdate("窗口步数: " + stepTimestamps.size());
+ }
+
+ // 如果在窗口内连续检测到足够步数
+ if (stepTimestamps.size() >= STEP_THRESHOLD) {
+ Log.w(TAG, "检测到用户正在走路 (步数模式)");
+ triggerCallback();
+ stepTimestamps.clear();
+ }
+ }
+
+ /**
+ * 方案二:基于加速度方差的算法
+ */
+ private void handleAccelDetection(SensorEvent event) {
+ float x = event.values[0];
+ float y = event.values[1];
+ float z = event.values[2];
+
+ // 计算合加速度模长
+ float magnitude;
+ if (event.sensor.getType() == Sensor.TYPE_LINEAR_ACCELERATION) {
+ magnitude = (float) Math.sqrt(x * x + y * y + z * z);
+ } else {
+ // 普通加速度计减去重力近似值
+ magnitude = (float) Math.abs(Math.sqrt(x * x + y * y + z * z) - 9.8);
+ }
+
+ magnitudeWindow.add(magnitude);
+ if (magnitudeWindow.size() > VARIANCE_WINDOW_SIZE) {
+ magnitudeWindow.poll();
+ }
+
+ // 窗口填满后按固定频率计算方差
+ long currentTime = System.currentTimeMillis();
+ if (magnitudeWindow.size() == VARIANCE_WINDOW_SIZE && (currentTime - lastVarianceEvalTime > VARIANCE_EVAL_INTERVAL)) {
+ lastVarianceEvalTime = currentTime;
+ double variance = calculateVariance(magnitudeWindow);
+
+ if (variance > VARIANCE_THRESHOLD) {
+ consecutiveShakeCount++;
+ String info = String.format("抖动方差: %.2f, 连续计数: %d", variance, consecutiveShakeCount);
+ Log.d(TAG, info);
+ if (listener != null) {
+ listener.onStatusUpdate(info);
+ }
+ if (consecutiveShakeCount >= CONSECUTIVE_THRESHOLD) {
+ Log.w(TAG, "检测到用户正在走路 (加速度方差模式)");
+ triggerCallback();
+ consecutiveShakeCount = 0;
+ magnitudeWindow.clear();
+ }
+ } else {
+ // 没达到阈值时,清空计数,要求必须是连续的抖动
+ consecutiveShakeCount = 0;
+ }
+ }
+ }
+
+ private double calculateVariance(Queue queue) {
+ double sum = 0;
+ for (float val : queue) {
+ sum += val;
+ }
+ double mean = sum / queue.size();
+
+ double temp = 0;
+ for (float val : queue) {
+ temp += (val - mean) * (val - mean);
+ }
+ return temp / queue.size();
+ }
+
+ private void triggerCallback() {
+ if (listener != null) {
+ listener.onShakeDetected();
+ }
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ }
+}
diff --git a/app/src/main/java/com/aoleyun/sn/service/DetectionService.java b/app/src/main/java/com/aoleyun/sn/service/DetectionService.java
new file mode 100644
index 0000000..ff2912c
--- /dev/null
+++ b/app/src/main/java/com/aoleyun/sn/service/DetectionService.java
@@ -0,0 +1,316 @@
+package com.aoleyun.sn.service;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.PointF;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.media.Image;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.OrientationEventListener;
+import android.view.Surface;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.ExperimentalGetImage;
+import androidx.camera.core.ImageAnalysis;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.core.content.ContextCompat;
+import androidx.lifecycle.LifecycleService;
+
+import com.aoleyun.sn.activity.CameraDetectionActivity;
+import com.aoleyun.sn.activity.WarningDialogActivity;
+import com.aoleyun.sn.detection.DistanceReminderManager;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.mlkit.vision.common.InputImage;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * 后台检测服务,用于持续监控人眼距离
+ * 移植自 CameraDetectionActivity
+ */
+public class DetectionService extends LifecycleService {
+ private static final String TAG = "DetectionService";
+
+ private ListenableFuture cameraProviderFuture;
+ private ExecutorService cameraExecutor;
+ private DistanceReminderManager reminderManager;
+ private ImageAnalysis imageAnalysis;
+ private OrientationEventListener orientationEventListener;
+
+ private SensorManager sensorManager;
+ private Sensor proximitySensor;
+
+ private long lastAnalyzedTimestamp = 0L;
+ private final Handler watchdogHandler = new Handler(Looper.getMainLooper());
+ private static final long WATCHDOG_INTERVAL = 10000; // 10秒检测一次
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.d(TAG, "onCreate");
+
+ // 初始化用于处理图像分析的单线程线程池
+ cameraExecutor = Executors.newSingleThreadExecutor();
+
+ // 初始化距离检测管理器
+ reminderManager = new DistanceReminderManager(new DistanceReminderManager.OnDistanceAlertListener() {
+ @Override
+ public void onTooClose() {
+ // 触发提醒
+ WarningDialogActivity.start(DetectionService.this, WarningDialogActivity.TYPE_DISTANCE);
+ }
+
+ @Override
+ public void onDistanceUpdate(double distance, PointF leftEye, PointF rightEye) {
+ // 后台服务通常不需要更新 UI Overlay,如果需要可以将数据通过 LiveEventBus 或广播发送出去
+ }
+ });
+
+ // 启动相机(系统应用假设已获得权限)
+ startCamera();
+
+ // 监听屏幕旋转,优化检测准确性
+ initOrientationListener();
+
+ // 启动看门狗,定时检查摄像头状态
+ startWatchdog();
+
+ // 初始化传感器检测
+ initProximitySensor();
+ }
+
+ private void initProximitySensor() {
+ sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
+ if (sensorManager != null) {
+ proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
+ if (proximitySensor != null) {
+ sensorManager.registerListener(proximityListener, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
+ Log.d(TAG, "已注册距离传感器监听");
+ } else {
+ Log.d(TAG, "未找到距离传感器");
+ }
+ }
+ }
+
+ private final SensorEventListener proximityListener = new SensorEventListener() {
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ if (CameraDetectionActivity.isShowing) {
+ return;
+ }
+ if (event.sensor.getType() == Sensor.TYPE_PROXIMITY) {
+ float distance = event.values[0];
+ // 大多数接近传感器是二值的(0或5),或者返回实际厘米数
+ // 这里判断如果小于最大范围则认为过近
+ if (distance < proximitySensor.getMaximumRange()) {
+ Log.d(TAG, "距离传感器检测到距离过近: " + distance);
+// WarningDialogActivity.start(DetectionService.this, WarningDialogActivity.TYPE_DISTANCE);
+ }
+ }
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ }
+ };
+
+ private void initOrientationListener() {
+ orientationEventListener = new OrientationEventListener(this) {
+ @Override
+ public void onOrientationChanged(int orientation) {
+ if (orientation == ORIENTATION_UNKNOWN) return;
+
+ // 将角度转换为 Surface.ROTATION
+ int rotation;
+ if (orientation >= 45 && orientation < 135) {
+ rotation = Surface.ROTATION_270;
+ } else if (orientation >= 135 && orientation < 225) {
+ rotation = Surface.ROTATION_180;
+ } else if (orientation >= 225 && orientation < 315) {
+ rotation = Surface.ROTATION_90;
+ } else {
+ rotation = Surface.ROTATION_0;
+ }
+
+ if (imageAnalysis != null) {
+ imageAnalysis.setTargetRotation(rotation);
+ }
+ }
+ };
+ orientationEventListener.enable();
+ }
+
+ private void startCamera() {
+ cameraProviderFuture = ProcessCameraProvider.getInstance(this);
+ cameraProviderFuture.addListener(() -> {
+ try {
+ ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
+ bindCameraUseCases(cameraProvider);
+ } catch (ExecutionException | InterruptedException e) {
+ Log.e(TAG, "初始化 CameraProvider 失败", e);
+ }
+ }, ContextCompat.getMainExecutor(this));
+ }
+
+ private void bindCameraUseCases(@NonNull ProcessCameraProvider cameraProvider) {
+ if (CameraDetectionActivity.isShowing) {
+ Log.d(TAG, "CameraDetectionActivity 正在显示,跳过后台相机绑定");
+ return;
+ }
+ // 绑定时更新时间戳,给予缓冲期
+ lastAnalyzedTimestamp = System.currentTimeMillis();
+
+ // 1. 选择【前置摄像头】
+ CameraSelector cameraSelector = new CameraSelector.Builder()
+ .requireLensFacing(CameraSelector.LENS_FACING_FRONT)
+ .build();
+
+ // 2. 创建图像分析用例(无需 Preview 用例以节省资源)
+ ImageAnalysis.Builder builder = new ImageAnalysis.Builder()
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
+ .setTargetResolution(new android.util.Size(640, 480));
+
+ // 设置初始旋转方向
+ WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
+ if (windowManager != null) {
+ builder.setTargetRotation(windowManager.getDefaultDisplay().getRotation());
+ }
+
+ imageAnalysis = builder.build();
+
+ // 3. 设置分析器
+ imageAnalysis.setAnalyzer(cameraExecutor, new ImageAnalysis.Analyzer() {
+ @OptIn(markerClass = ExperimentalGetImage.class)
+ @Override
+ public void analyze(@NonNull ImageProxy imageProxy) {
+ // 限制频率,每秒执行一次算法
+ long currentTimestamp = System.currentTimeMillis();
+ if (currentTimestamp - DetectionService.this.lastAnalyzedTimestamp < 1000) {
+ imageProxy.close();
+ return;
+ }
+ DetectionService.this.lastAnalyzedTimestamp = currentTimestamp;
+
+ Image mediaImage = imageProxy.getImage();
+ if (mediaImage != null) {
+ int rotationDegrees = imageProxy.getImageInfo().getRotationDegrees();
+ InputImage inputImage = InputImage.fromMediaImage(mediaImage, rotationDegrees);
+
+ // 调用检测逻辑
+ reminderManager.checkDistance(inputImage)
+ .addOnCompleteListener(task -> imageProxy.close());
+ } else {
+ imageProxy.close();
+ }
+ }
+ });
+
+ try {
+ // 在绑定前先解绑所有用例
+ cameraProvider.unbindAll();
+ // 将图像分析用例绑定到 LifecycleService 的生命周期
+ cameraProvider.bindToLifecycle(this, cameraSelector, imageAnalysis);
+ Log.d(TAG, "相机用例绑定成功");
+ } catch (Exception e) {
+ Log.e(TAG, "绑定相机用例失败", e);
+ }
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ super.onStartCommand(intent, flags, startId);
+ return START_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ Log.d(TAG, "onDestroy");
+ if (cameraExecutor != null) {
+ cameraExecutor.shutdown();
+ }
+ if (orientationEventListener != null) {
+ orientationEventListener.disable();
+ }
+ if (sensorManager != null) {
+ sensorManager.unregisterListener(proximityListener);
+ }
+ stopWatchdog();
+ }
+
+ private void startWatchdog() {
+ watchdogHandler.postDelayed(watchdogRunnable, WATCHDOG_INTERVAL);
+ }
+
+ private void stopWatchdog() {
+ watchdogHandler.removeCallbacks(watchdogRunnable);
+ }
+
+ private final Runnable watchdogRunnable = new Runnable() {
+ @Override
+ public void run() {
+ checkCameraStatus();
+ watchdogHandler.postDelayed(this, WATCHDOG_INTERVAL);
+ }
+ };
+
+ /**
+ * 定时检查摄像头状态,如果被抢占或未正常运行则尝试重新绑定
+ */
+ private void checkCameraStatus() {
+ if (CameraDetectionActivity.isShowing) {
+ // 如果检测界面正在显示,后台服务主动释放摄像头,避免抢占
+ releaseCameraIfBound();
+ return;
+ }
+
+ if (cameraProviderFuture == null || !cameraProviderFuture.isDone()) {
+ return;
+ }
+
+ try {
+ ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
+ if (cameraProvider == null) return;
+
+ boolean isBound = false;
+ if (imageAnalysis != null) {
+ isBound = cameraProvider.isBound(imageAnalysis);
+ }
+
+ long now = System.currentTimeMillis();
+ // 如果未绑定,或者已绑定但超过 15 秒没有收到图像帧(说明摄像头可能被抢占或异常)
+ if (!isBound || (now - lastAnalyzedTimestamp > 15000)) {
+ Log.w(TAG, "检测到摄像头未运行或可能被抢占,尝试重新绑定... isBound=" + isBound);
+ bindCameraUseCases(cameraProvider);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "检查摄像头状态失败", e);
+ }
+ }
+
+ private void releaseCameraIfBound() {
+ if (cameraProviderFuture == null || !cameraProviderFuture.isDone()) {
+ return;
+ }
+ try {
+ ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
+ if (imageAnalysis != null && cameraProvider.isBound(imageAnalysis)) {
+ Log.d(TAG, "检测到 CameraDetectionActivity 正在运行,释放后台摄像头绑定");
+ cameraProvider.unbind(imageAnalysis);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "释放摄像头失败", e);
+ }
+ }
+}
diff --git a/app/src/main/java/com/aoleyun/sn/service/ShakeDetectionService.java b/app/src/main/java/com/aoleyun/sn/service/ShakeDetectionService.java
new file mode 100644
index 0000000..49352c3
--- /dev/null
+++ b/app/src/main/java/com/aoleyun/sn/service/ShakeDetectionService.java
@@ -0,0 +1,50 @@
+package com.aoleyun.sn.service;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.aoleyun.sn.activity.WarningDialogActivity;
+import com.aoleyun.sn.detection.ShakeDetectionManager;
+
+public class ShakeDetectionService extends Service {
+ private static final String TAG = "ShakeDetectionService";
+ private ShakeDetectionManager shakeDetectionManager;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.e(TAG, "onCreate: ");
+ shakeDetectionManager = new ShakeDetectionManager(this, new ShakeDetectionManager.OnShakeListener() {
+ @Override
+ public void onShakeDetected() {
+ // 后台提醒
+ WarningDialogActivity.start(ShakeDetectionService.this, WarningDialogActivity.TYPE_SHAKE);
+ }
+ });
+ shakeDetectionManager.start();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ return START_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ Log.e(TAG, "onDestroy: ");
+ if (shakeDetectionManager != null) {
+ shakeDetectionManager.stop();
+ }
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+}
diff --git a/app/src/main/java/com/aoleyun/sn/service/main/MainService.java b/app/src/main/java/com/aoleyun/sn/service/main/MainService.java
index dd1a386..195e9d1 100644
--- a/app/src/main/java/com/aoleyun/sn/service/main/MainService.java
+++ b/app/src/main/java/com/aoleyun/sn/service/main/MainService.java
@@ -49,10 +49,13 @@ import com.aoleyun.sn.bean.ScreenLockState;
import com.aoleyun.sn.comm.CommonConfig;
import com.aoleyun.sn.comm.JGYActions;
import com.aoleyun.sn.comm.PackageNames;
+import com.aoleyun.sn.detection.SensorCheckUtil;
import com.aoleyun.sn.gson.GsonUtils;
import com.aoleyun.sn.hook.AoleyunActivityController;
import com.aoleyun.sn.network.NetInterfaceManager;
import com.aoleyun.sn.rlog.LogDBManager;
+import com.aoleyun.sn.service.DetectionService;
+import com.aoleyun.sn.service.ShakeDetectionService;
import com.aoleyun.sn.utils.ApkUtils;
import com.aoleyun.sn.utils.CacheUtils;
import com.aoleyun.sn.utils.ForegroundAppUtil;
@@ -481,6 +484,21 @@ public class MainService extends BaseService implements NetworkUtils.OnNetworkSt
if (TextUtils.isEmpty(allowAppList)) {
Settings.System.putString(getContentResolver(), CommonConfig.SEEWO_APP_SOURCE_INSTALL_WHITELIST, String.join(",", JgyUtils.DEFAULT_APP_SOURCE_WHITE_LIST));
}
+
+
+ int reminder = mMMKV.decodeInt(CommonConfig.EYE_TO_SCREEN_DISTANCE_REMINDER, 0);
+ Log.e(TAG, "onCreate: reminder = " + reminder);
+ if (reminder == 1) {
+ if (!SensorCheckUtil.hasProximitySensor(this)) {
+ startService(new Intent(this, DetectionService.class));
+ }
+ }
+
+ int shakeReminder = mMMKV.decodeInt(CommonConfig.SHAKE_REMINDER, 0);
+ Log.e(TAG, "onCreate: shakeReminder = " + shakeReminder);
+ if (shakeReminder == 1) {
+ startService(new Intent(this, ShakeDetectionService.class));
+ }
}
private void initLiveData() {
diff --git a/app/src/main/java/com/aoleyun/sn/service/main/MainServiceModel.java b/app/src/main/java/com/aoleyun/sn/service/main/MainServiceModel.java
index 5a1927b..f107cd9 100644
--- a/app/src/main/java/com/aoleyun/sn/service/main/MainServiceModel.java
+++ b/app/src/main/java/com/aoleyun/sn/service/main/MainServiceModel.java
@@ -238,15 +238,13 @@ public class MainServiceModel extends ViewModel {
}
public void checkTestUpdate() {
- if (!JgyUtils.getInstance().tagEmpty()) {
- NetInterfaceManager.getInstance()
- .checkTestUpdate(getLifecycle(), new NetInterfaceManager.onCompleteCallback() {
- @Override
- public void onComplete() {
+ NetInterfaceManager.getInstance()
+ .checkTestUpdate(getLifecycle(), new NetInterfaceManager.onCompleteCallback() {
+ @Override
+ public void onComplete() {
- }
- });
- }
+ }
+ });
}
public void getDeveloper() {
diff --git a/app/src/main/java/com/aoleyun/sn/view/FaceOverlayView.java b/app/src/main/java/com/aoleyun/sn/view/FaceOverlayView.java
new file mode 100644
index 0000000..f100501
--- /dev/null
+++ b/app/src/main/java/com/aoleyun/sn/view/FaceOverlayView.java
@@ -0,0 +1,95 @@
+package com.aoleyun.sn.view;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import java.util.Locale;
+
+public class FaceOverlayView extends View {
+ private Paint pointPaint;
+ private Paint textPaint;
+ private PointF leftEye;
+ private PointF rightEye;
+ private double distance;
+ private int imageWidth;
+ private int imageHeight;
+
+ public FaceOverlayView(Context context) {
+ super(context);
+ init();
+ }
+
+ public FaceOverlayView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ private void init() {
+ pointPaint = new Paint();
+ pointPaint.setColor(Color.RED);
+ pointPaint.setStyle(Paint.Style.FILL);
+ pointPaint.setStrokeWidth(10f);
+
+ textPaint = new Paint();
+ textPaint.setColor(Color.YELLOW);
+ textPaint.setTextSize(60f);
+ textPaint.setFakeBoldText(true);
+ textPaint.setShadowLayer(5, 0, 0, Color.BLACK);
+ }
+
+ public void updateData(double distance, PointF leftEye, PointF rightEye, int imageWidth, int imageHeight) {
+ this.distance = distance;
+ this.leftEye = leftEye;
+ this.rightEye = rightEye;
+ this.imageWidth = imageWidth;
+ this.imageHeight = imageHeight;
+ postInvalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (leftEye == null || rightEye == null || imageWidth <= 0 || imageHeight <= 0) {
+ canvas.drawText("未检测到人脸", 50, 100, textPaint);
+ return;
+ }
+
+ // 1. 获取 View 的尺寸
+ float viewWidth = getWidth();
+ float viewHeight = getHeight();
+
+ // 2. 计算缩放比例 (适配 PreviewView 的 FILL_CENTER 逻辑)
+ // FILL_CENTER:取宽高缩放比例中较大的一个,使图像填满 View 且不拉伸,多出的部分被裁剪
+ float scale = Math.max(viewWidth / imageWidth, viewHeight / imageHeight);
+
+ // 3. 计算偏移量(由居中裁剪产生)
+ float postScaleWidth = imageWidth * scale;
+ float postScaleHeight = imageHeight * scale;
+ float offsetX = (viewWidth - postScaleWidth) / 2f;
+ float offsetY = (viewHeight - postScaleHeight) / 2f;
+
+ // 4. 坐标映射并处理镜像
+ // CameraX 的 PreviewView 显示前置摄像头时默认是镜像的
+ // 映射公式: viewX = viewWidth - (imageX * scale + offsetX)
+ float lx = viewWidth - (leftEye.x * scale + offsetX);
+ float ly = leftEye.y * scale + offsetY;
+ float rx = viewWidth - (rightEye.x * scale + offsetX);
+ float ry = rightEye.y * scale + offsetY;
+
+ // 绘制眼睛点位
+ canvas.drawCircle(lx, ly, 15, pointPaint);
+ canvas.drawCircle(rx, ry, 15, pointPaint);
+
+ // 绘制连线,更直观
+ canvas.drawLine(lx, ly, rx, ry, pointPaint);
+
+ canvas.drawText(String.format(Locale.getDefault(), "瞳距: %.2f", distance), 50, 100, textPaint);
+ }
+}
diff --git a/app/src/main/java/com/aoleyun/sn/view/ToggleButton.java b/app/src/main/java/com/aoleyun/sn/view/ToggleButton.java
index 76d9c5a..ecc64db 100644
--- a/app/src/main/java/com/aoleyun/sn/view/ToggleButton.java
+++ b/app/src/main/java/com/aoleyun/sn/view/ToggleButton.java
@@ -28,11 +28,11 @@ public class ToggleButton extends View {
/**
* 开启颜色
*/
- private int onColor = Color.parseColor("#00d56b");
+ private int onColor = Color.parseColor("#3a56e1");
/**
* 关闭颜色
*/
- private int offBorderColor = Color.parseColor("#e7e4e4");
+ private int offBorderColor = Color.parseColor("#727272");
/**
* 灰色带颜色
*/
@@ -56,7 +56,7 @@ public class ToggleButton extends View {
/**
* 边框大小
*/
- private int borderWidth = 2;
+ private int borderWidth = 3;
/**
* 垂直中心
*/
diff --git a/app/src/main/res/drawable/bg_warning_btn_blue.xml b/app/src/main/res/drawable/bg_warning_btn_blue.xml
new file mode 100644
index 0000000..9d65829
--- /dev/null
+++ b/app/src/main/res/drawable/bg_warning_btn_blue.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_warning.xml b/app/src/main/res/drawable/ic_warning.xml
new file mode 100644
index 0000000..792b674
--- /dev/null
+++ b/app/src/main/res/drawable/ic_warning.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_warning_shake.xml b/app/src/main/res/drawable/ic_warning_shake.xml
new file mode 100644
index 0000000..9b50a35
--- /dev/null
+++ b/app/src/main/res/drawable/ic_warning_shake.xml
@@ -0,0 +1,24 @@
+
+
+
+
diff --git a/app/src/main/res/layout/activity_eye_protection.xml b/app/src/main/res/layout-land/activity_eye_protection.xml
similarity index 57%
rename from app/src/main/res/layout/activity_eye_protection.xml
rename to app/src/main/res/layout-land/activity_eye_protection.xml
index ab50e54..2506e93 100644
--- a/app/src/main/res/layout/activity_eye_protection.xml
+++ b/app/src/main/res/layout-land/activity_eye_protection.xml
@@ -6,6 +6,9 @@
+
-
-
-
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+
+
+
+
+
-
-
-
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-port/activity_eye_protection.xml b/app/src/main/res/layout-port/activity_eye_protection.xml
new file mode 100644
index 0000000..f9f74fa
--- /dev/null
+++ b/app/src/main/res/layout-port/activity_eye_protection.xml
@@ -0,0 +1,426 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_camera_detection.xml b/app/src/main/res/layout/activity_camera_detection.xml
new file mode 100644
index 0000000..e9ee065
--- /dev/null
+++ b/app/src/main/res/layout/activity_camera_detection.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_warning_dialog.xml b/app/src/main/res/layout/activity_warning_dialog.xml
new file mode 100644
index 0000000..0f6ef08
--- /dev/null
+++ b/app/src/main/res/layout/activity_warning_dialog.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index c0cd961..50e4b39 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -95,4 +95,28 @@
- true
- true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+