diff --git a/app/src/main/java/com/ouxuan/oxface/OXFaceOnlineActivity.java b/app/src/main/java/com/ouxuan/oxface/OXFaceOnlineActivity.java index e6792db..a96896a 100644 --- a/app/src/main/java/com/ouxuan/oxface/OXFaceOnlineActivity.java +++ b/app/src/main/java/com/ouxuan/oxface/OXFaceOnlineActivity.java @@ -172,8 +172,11 @@ public class OXFaceOnlineActivity extends BaseActivity implements View.OnClickLi private TextView tvStoreName; private int storeNameClickCount = 0; private long lastStoreNameClickTime = 0; - private static final int MAX_CLICK_COUNT = 5; + private static final int MAX_CLICK_COUNT = 6; // 修改为6次 private static final long CLICK_INTERVAL = 1000; // 1秒内点击有效 + + // 新增解锁密码弹窗相关变量 + private com.ouxuan.oxface.device.UnlockPasswordDialog unlockPasswordDialog; // 新增订单查验相关变量 private int modeType = 0; // 1验证码验证 2人脸验证 3扫码验证 4扫码器验证 @@ -222,6 +225,9 @@ public class OXFaceOnlineActivity extends BaseActivity implements View.OnClickLi // 初始化摄像头控制广播接收器 initCameraControlReceiver(); + // 初始化解锁密码弹窗 + initUnlockPasswordDialog(); + initView(); // 初始化网络状态指示器 @@ -846,6 +852,15 @@ public class OXFaceOnlineActivity extends BaseActivity implements View.OnClickLi } }); + // 设置解锁密码弹窗触发器 + gateUnavailableDialog.setUnlockTrigger(new GateUnavailableDialog.UnlockPasswordDialogTrigger() { + @Override + public void triggerUnlockPasswordDialog() { + LogManager.logInfo(TAG, "门禁不可用弹窗标题触发解锁密码弹窗"); + showUnlockPasswordDialog(); + } + }); + // 检查是否开启AB门检测 checkGateCheckEnabled(); @@ -1458,6 +1473,13 @@ public class OXFaceOnlineActivity extends BaseActivity implements View.OnClickLi LogManager.logInfo(TAG, "摄像头控制广播接收器已注销"); } + // 释放解锁密码弹窗资源 + if (unlockPasswordDialog != null) { + unlockPasswordDialog.release(); + unlockPasswordDialog = null; + LogManager.logInfo(TAG, "解锁密码弹窗资源已释放"); + } + // 停止摄像头预览 if (CameraPreviewManager.getInstance() != null) { CameraPreviewManager.getInstance().stopPreview(); @@ -2143,37 +2165,87 @@ public class OXFaceOnlineActivity extends BaseActivity implements View.OnClickLi LogManager.logDebug(TAG, "店铺名称被点击,当前计数: " + storeNameClickCount); - // 如果连续点击达到5次 + // 如果连续点击达到6次,触发解锁密码弹窗 if (storeNameClickCount >= MAX_CLICK_COUNT) { - LogManager.logInfo(TAG, "连续点击5次店铺名称,准备返回主界面"); - Toast.makeText(this, "即将返回主界面", Toast.LENGTH_SHORT).show(); - + LogManager.logInfo(TAG, "连续点击6次店铺名称,触发解锁密码弹窗"); + // 重置计数器 storeNameClickCount = 0; - - // 延迟一段时间后返回主界面 - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - returnToMainActivity(); - } - }, 500); // 延迟0.5秒后返回 + + // 显示解锁密码弹窗 + showUnlockPasswordDialog(); } else if (storeNameClickCount >= 3) { // 当点击次数达到3次时,提示用户还需几次点击 int remainingClicks = MAX_CLICK_COUNT - storeNameClickCount; - Toast.makeText(this, "再点击" + remainingClicks + "次返回主界面", Toast.LENGTH_SHORT).show(); + Toast.makeText(this, "再点击" + remainingClicks + "次触发解锁", Toast.LENGTH_SHORT).show(); } } /** - * 返回主界面 + * 初始化解锁密码弹窗 */ - private void returnToMainActivity() { - LogManager.logInfo(TAG, "返回主界面"); - Intent intent = new Intent(this, MainActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - finish(); + private void initUnlockPasswordDialog() { + unlockPasswordDialog = new com.ouxuan.oxface.device.UnlockPasswordDialog(this); + unlockPasswordDialog.setListener(new com.ouxuan.oxface.device.UnlockPasswordDialog.UnlockDialogListener() { + @Override + public void onUnlockSuccess() { + LogManager.logInfo(TAG, "解锁成功,准备退出人脸识别"); + unLockAndLeaveFaceDetect(); + } + + @Override + public void onDialogShow() { + // 暂停摄像头预览 + pauseCameraWithTimeout(); + LogManager.logInfo(TAG, "解锁密码弹窗显示,摄像头已暂停"); + } + + @Override + public void onDialogDismiss() { + // 恢复摄像头预览 + resumeCamera(); + LogManager.logInfo(TAG, "解锁密码弹窗关闭,摄像头已恢复"); + } + }); + + LogManager.logInfo(TAG, "解锁密码弹窗初始化完成"); + } + + /** + * 显示解锁密码弹窗 + */ + private void showUnlockPasswordDialog() { + if (unlockPasswordDialog != null && !unlockPasswordDialog.isShowing()) { + unlockPasswordDialog.show(); + } + } + + /** + * 解锁并退出人脸识别界面 + */ + private void unLockAndLeaveFaceDetect() { + LogManager.logInfo(TAG, "执行解锁退出操作"); + + try { + // 停止摄像头预览 + if (CameraPreviewManager.getInstance() != null) { + CameraPreviewManager.getInstance().stopPreview(); + } + + // 返回登录界面 + Intent intent = new Intent(this, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + + // 关闭当前界面 + finish(); + + LogManager.logInfo(TAG, "已退出人脸识别界面,返回登录界面"); + + } catch (Exception e) { + LogManager.logError(TAG, "解锁退出操作失败", e); + Toast.makeText(this, "退出失败,请重试", Toast.LENGTH_SHORT).show(); + } } /** diff --git a/app/src/main/java/com/ouxuan/oxface/abgate/GateUnavailableDialog.java b/app/src/main/java/com/ouxuan/oxface/abgate/GateUnavailableDialog.java index 48c81f1..4b2b384 100644 --- a/app/src/main/java/com/ouxuan/oxface/abgate/GateUnavailableDialog.java +++ b/app/src/main/java/com/ouxuan/oxface/abgate/GateUnavailableDialog.java @@ -97,6 +97,21 @@ public class GateUnavailableDialog { } } + // 解锁密码弹窗相关变量 + private int titleClickCount = 0; + private long lastTitleClickTime = 0; + private static final int TITLE_MAX_CLICK_COUNT = 6; + private static final long TITLE_CLICK_INTERVAL = 1000; // 1秒内点击有效 + /** + * 解锁密码弹窗触发器接口 + */ + public interface UnlockPasswordDialogTrigger { + /** + * 触发解锁密码弹窗 + */ + void triggerUnlockPasswordDialog(); + } + private GateUnavailableDialogListener dialogListener; private boolean isShowing = false; @@ -108,6 +123,14 @@ public class GateUnavailableDialog { this.dialogListener = listener; } + /** + * 设置解锁密码弹窗触发器 + * @param trigger 触发器 + */ + public void setUnlockTrigger(UnlockPasswordDialogTrigger trigger) { + this.unlockTrigger = trigger; + } + public GateUnavailableDialog(Context context) { this.context = context; // 初始化线程池和处理器 @@ -150,6 +173,14 @@ public class GateUnavailableDialog { tvTitle.setGravity(Gravity.CENTER); tvTitle.setPadding(0, 0, 0, 20); + // 为标题添加点击监听器(用于触发解锁密码弹窗) + tvTitle.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + handleTitleClick(); + } + }); + // 消息内容 tvMessage = new TextView(context); tvMessage.setTextSize(16); @@ -211,6 +242,43 @@ public class GateUnavailableDialog { } /** + * 处理标题点击事件(用于触发解锁密码弹窗) + */ + private void handleTitleClick() { + long currentTime = System.currentTimeMillis(); + + // 检查点击间隔,如果超过时间间隔则重置计数器 + if (currentTime - lastTitleClickTime > TITLE_CLICK_INTERVAL) { + titleClickCount = 0; + } + + titleClickCount++; + lastTitleClickTime = currentTime; + + LogManager.logDebug(TAG, "门禁不可用弹窗标题被点击,当前计数: " + titleClickCount); + + // 如果连续点击达到6次,触发解锁密码弹窗 + if (titleClickCount >= TITLE_MAX_CLICK_COUNT) { + LogManager.logInfo(TAG, "连续点击6次门禁不可用弹窗标题,触发解锁密码弹窗"); + + // 重置计数器 + titleClickCount = 0; + + // 触发解锁密码弹窗 + if (unlockTrigger != null) { + unlockTrigger.triggerUnlockPasswordDialog(); + } else { + LogManager.logWarning(TAG, "unlockTrigger为空,无法触发解锁密码弹窗"); + } + } else if (titleClickCount >= 3) { + // 当点击次数达到3次时,提示用户还需几次点击 + int remainingClicks = TITLE_MAX_CLICK_COUNT - titleClickCount; + // 这里不显示Toast,因为可能会干扰弹窗显示 + LogManager.logInfo(TAG, "还需点击" + remainingClicks + "次触发解锁"); + } + } + + /** * 更新门状态显示 * @param gateAOpen A门是否开启 * @param gateBOpen B门是否开启 diff --git a/app/src/main/java/com/ouxuan/oxface/device/UnlockPasswordDialog.java b/app/src/main/java/com/ouxuan/oxface/device/UnlockPasswordDialog.java new file mode 100644 index 0000000..be28b48 --- /dev/null +++ b/app/src/main/java/com/ouxuan/oxface/device/UnlockPasswordDialog.java @@ -0,0 +1,412 @@ +package com.ouxuan.oxface.device; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.text.InputType; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.ouxuan.oxface.data.LoginDataManager; +import com.ouxuan.oxface.utils.LogManager; + +/** + * 解锁密码弹窗 + * 用于特殊情况下退出人脸识别界面 + */ +public class UnlockPasswordDialog { + + private static final String TAG = "UnlockPasswordDialog"; + + private Context context; + private Dialog dialog; + private EditText etPassword; + private TextView tvErrorTip; + private Button btnConfirm, btnCancel; + private LoginDataManager loginDataManager; + private UnlockDialogListener listener; + + /** + * 解锁弹窗监听器 + */ + public interface UnlockDialogListener { + /** + * 解锁成功回调 + */ + void onUnlockSuccess(); + + /** + * 弹窗显示时回调(暂停摄像头) + */ + void onDialogShow(); + + /** + * 弹窗关闭时回调(恢复摄像头) + */ + void onDialogDismiss(); + } + + public UnlockPasswordDialog(Context context) { + this.context = context; + this.loginDataManager = LoginDataManager.getInstance(context); + createDialog(); + } + + /** + * 设置监听器 + * @param listener 监听器 + */ + public void setListener(UnlockDialogListener listener) { + this.listener = listener; + } + + /** + * 创建弹窗 + */ + private void createDialog() { + dialog = new Dialog(context); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + + // 创建主布局 + LinearLayout mainLayout = new LinearLayout(context); + mainLayout.setOrientation(LinearLayout.VERTICAL); + mainLayout.setBackgroundColor(Color.parseColor("#CC000000")); // 半透明黑色背景 + mainLayout.setGravity(Gravity.CENTER); + mainLayout.setPadding(40, 40, 40, 40); + + // 内容容器 + LinearLayout contentLayout = new LinearLayout(context); + contentLayout.setOrientation(LinearLayout.VERTICAL); + contentLayout.setBackgroundColor(Color.WHITE); + contentLayout.setPadding(40, 30, 40, 30); + contentLayout.setGravity(Gravity.CENTER); + + // 设置圆角效果 + android.graphics.drawable.GradientDrawable drawable = new android.graphics.drawable.GradientDrawable(); + drawable.setColor(Color.WHITE); + drawable.setCornerRadius(20); + contentLayout.setBackground(drawable); + + // 标题 + TextView tvTitle = new TextView(context); + tvTitle.setText("解锁验证"); + tvTitle.setTextSize(20); + tvTitle.setTextColor(Color.parseColor("#333333")); + tvTitle.setGravity(Gravity.CENTER); + tvTitle.setPadding(0, 0, 0, 20); + + // 提示文字 + TextView tvHint = new TextView(context); + tvHint.setText("请输入登录密码以退出人脸识别"); + tvHint.setTextSize(14); + tvHint.setTextColor(Color.parseColor("#666666")); + tvHint.setGravity(Gravity.CENTER); + tvHint.setPadding(0, 0, 0, 15); + + // 密码输入框 + etPassword = new EditText(context); + etPassword.setHint("请输入密码"); + etPassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + etPassword.setTextSize(16); + etPassword.setTextColor(Color.parseColor("#333333")); + etPassword.setHintTextColor(Color.parseColor("#999999")); + etPassword.setPadding(20, 15, 20, 15); + etPassword.setBackgroundColor(Color.parseColor("#F5F5F5")); + + // 设置输入框圆角 + android.graphics.drawable.GradientDrawable inputDrawable = new android.graphics.drawable.GradientDrawable(); + inputDrawable.setColor(Color.parseColor("#F5F5F5")); + inputDrawable.setCornerRadius(10); + inputDrawable.setStroke(2, Color.parseColor("#E0E0E0")); + etPassword.setBackground(inputDrawable); + + LinearLayout.LayoutParams inputParams = new LinearLayout.LayoutParams( + (int)(280 * context.getResources().getDisplayMetrics().density), + LinearLayout.LayoutParams.WRAP_CONTENT + ); + inputParams.setMargins(0, 0, 0, 10); + etPassword.setLayoutParams(inputParams); + + // 错误提示 + tvErrorTip = new TextView(context); + tvErrorTip.setTextSize(12); + tvErrorTip.setTextColor(Color.parseColor("#FF4444")); + tvErrorTip.setGravity(Gravity.CENTER); + tvErrorTip.setVisibility(View.GONE); + tvErrorTip.setPadding(0, 0, 0, 15); + + // 按钮容器 + LinearLayout buttonLayout = new LinearLayout(context); + buttonLayout.setOrientation(LinearLayout.HORIZONTAL); + buttonLayout.setGravity(Gravity.CENTER); + buttonLayout.setPadding(0, 20, 0, 0); + + // 取消按钮 + btnCancel = new Button(context); + btnCancel.setText("取消"); + btnCancel.setTextSize(16); + btnCancel.setTextColor(Color.parseColor("#666666")); + btnCancel.setBackgroundColor(Color.parseColor("#F0F0F0")); + + // 设置取消按钮圆角 + android.graphics.drawable.GradientDrawable cancelDrawable = new android.graphics.drawable.GradientDrawable(); + cancelDrawable.setColor(Color.parseColor("#F0F0F0")); + cancelDrawable.setCornerRadius(8); + btnCancel.setBackground(cancelDrawable); + + LinearLayout.LayoutParams cancelParams = new LinearLayout.LayoutParams( + (int)(100 * context.getResources().getDisplayMetrics().density), + (int)(45 * context.getResources().getDisplayMetrics().density) + ); + cancelParams.setMargins(0, 0, 20, 0); + btnCancel.setLayoutParams(cancelParams); + + // 确认按钮 + btnConfirm = new Button(context); + btnConfirm.setText("确认"); + btnConfirm.setTextSize(16); + btnConfirm.setTextColor(Color.WHITE); + btnConfirm.setBackgroundColor(Color.parseColor("#007AFF")); + + // 设置确认按钮圆角 + android.graphics.drawable.GradientDrawable confirmDrawable = new android.graphics.drawable.GradientDrawable(); + confirmDrawable.setColor(Color.parseColor("#007AFF")); + confirmDrawable.setCornerRadius(8); + btnConfirm.setBackground(confirmDrawable); + + LinearLayout.LayoutParams confirmParams = new LinearLayout.LayoutParams( + (int)(100 * context.getResources().getDisplayMetrics().density), + (int)(45 * context.getResources().getDisplayMetrics().density) + ); + btnConfirm.setLayoutParams(confirmParams); + + // 添加按钮到容器 + buttonLayout.addView(btnCancel); + buttonLayout.addView(btnConfirm); + + // 添加所有视图到内容容器 + contentLayout.addView(tvTitle); + contentLayout.addView(tvHint); + contentLayout.addView(etPassword); + contentLayout.addView(tvErrorTip); + contentLayout.addView(buttonLayout); + + // 设置内容布局参数 + LinearLayout.LayoutParams contentParams = new LinearLayout.LayoutParams( + (int)(320 * context.getResources().getDisplayMetrics().density), + LinearLayout.LayoutParams.WRAP_CONTENT + ); + contentParams.gravity = Gravity.CENTER; + mainLayout.addView(contentLayout, contentParams); + + // 设置Dialog属性 + dialog.setContentView(mainLayout); + + Window window = dialog.getWindow(); + if (window != null) { + // 设置全屏显示 + window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT); + window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + + // 设置窗口标志 + window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + + // 清除默认动画 + window.setWindowAnimations(0); + } + + // 设置不可取消 + dialog.setCancelable(false); + dialog.setCanceledOnTouchOutside(false); + + // 设置监听器 + setupListeners(); + } + + /** + * 设置监听器 + */ + private void setupListeners() { + // 取消按钮 + btnCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + LogManager.logInfo(TAG, "用户取消解锁"); + dismiss(); + } + }); + + // 确认按钮 + btnConfirm.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + handleConfirm(); + } + }); + + // 密码输入框回车监听 + etPassword.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, android.view.KeyEvent event) { + if (actionId == android.view.inputmethod.EditorInfo.IME_ACTION_DONE || + actionId == android.view.inputmethod.EditorInfo.IME_ACTION_GO || + (event != null && event.getKeyCode() == android.view.KeyEvent.KEYCODE_ENTER)) { + handleConfirm(); + return true; + } + return false; + } + }); + } + + /** + * 处理确认按钮点击 + */ + private void handleConfirm() { + String inputPassword = etPassword.getText().toString().trim(); + + if (inputPassword.isEmpty()) { + showErrorTip("请输入密码"); + return; + } + + // 获取保存的登录密码 + String savedPassword = loginDataManager.getPassword(); + + if (savedPassword == null || savedPassword.isEmpty()) { + LogManager.logError(TAG, "未找到保存的登录密码"); + showErrorTip("系统错误:未找到登录密码"); + return; + } + + // 验证密码 + if (inputPassword.equals(savedPassword)) { + LogManager.logInfo(TAG, "密码验证成功,准备解锁退出"); + hideErrorTip(); + + // 通知解锁成功 + if (listener != null) { + listener.onUnlockSuccess(); + } + + dismiss(); + } else { + LogManager.logWarning(TAG, "密码验证失败"); + showErrorTip("密码错误,请重新输入"); + + // 清空输入框 + etPassword.setText(""); + etPassword.requestFocus(); + } + } + + /** + * 显示错误提示 + * @param message 错误信息 + */ + private void showErrorTip(String message) { + tvErrorTip.setText(message); + tvErrorTip.setVisibility(View.VISIBLE); + } + + /** + * 隐藏错误提示 + */ + private void hideErrorTip() { + tvErrorTip.setVisibility(View.GONE); + } + + /** + * 显示弹窗 + */ + public void show() { + try { + if (dialog != null && !dialog.isShowing()) { + // 清空之前的输入和错误提示 + etPassword.setText(""); + hideErrorTip(); + + // 显示弹窗 + dialog.show(); + + // 请求输入框焦点 + etPassword.requestFocus(); + + // 显示软键盘 + android.view.inputmethod.InputMethodManager imm = + (android.view.inputmethod.InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.showSoftInput(etPassword, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT); + } + + LogManager.logInfo(TAG, "解锁密码弹窗已显示"); + + // 通知监听器弹窗显示 + if (listener != null) { + listener.onDialogShow(); + } + } + } catch (Exception e) { + LogManager.logError(TAG, "显示解锁密码弹窗失败", e); + } + } + + /** + * 关闭弹窗 + */ + public void dismiss() { + try { + if (dialog != null && dialog.isShowing()) { + // 隐藏软键盘 + android.view.inputmethod.InputMethodManager imm = + (android.view.inputmethod.InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.hideSoftInputFromWindow(etPassword.getWindowToken(), 0); + } + + dialog.dismiss(); + LogManager.logInfo(TAG, "解锁密码弹窗已关闭"); + + // 通知监听器弹窗关闭 + if (listener != null) { + listener.onDialogDismiss(); + } + } + } catch (Exception e) { + LogManager.logError(TAG, "关闭解锁密码弹窗失败", e); + } + } + + /** + * 检查弹窗是否正在显示 + * @return true表示正在显示 + */ + public boolean isShowing() { + return dialog != null && dialog.isShowing(); + } + + /** + * 释放资源 + */ + public void release() { + dismiss(); + + if (dialog != null) { + dialog = null; + } + + context = null; + listener = null; + loginDataManager = null; + + LogManager.logInfo(TAG, "解锁密码弹窗资源已释放"); + } +} \ No newline at end of file diff --git a/解锁密码弹窗功能实现说明.md b/解锁密码弹窗功能实现说明.md new file mode 100644 index 0000000..9da3456 --- /dev/null +++ b/解锁密码弹窗功能实现说明.md @@ -0,0 +1,208 @@ +# 解锁密码弹窗功能实现说明 + +## 功能概述 + +本次实现了解锁密码弹窗功能,为特殊情况下退出人脸识别界面提供了一个安全的出口。该功能完全符合用户需求,提供了美观简洁的界面和安全的密码验证机制。 + +## 实现的功能 + +### 1. 解锁密码弹窗类 (`UnlockPasswordDialog.java`) + +**位置**: `app/src/main/java/com/ouxuan/oxface/device/UnlockPasswordDialog.java` + +**主要特性**: +- 美观的弹窗界面设计,符合主题色 +- 安全的密码输入(显示为密码形式) +- 实时密码验证(与本地存储的登录密码对比) +- 错误提示功能(密码错误时显示红色提示) +- 摄像头控制集成(弹窗显示时暂停摄像头,关闭时恢复) +- 软键盘自动弹出和收起 +- 完整的生命周期管理 + +**核心方法**: +- `show()`: 显示弹窗 +- `dismiss()`: 关闭弹窗 +- `handleConfirm()`: 处理密码验证 +- `UnlockDialogListener`: 回调接口 + +### 2. 触发机制 + +#### 2.1 店铺名称连续点击触发 +**位置**: `OXFaceOnlineActivity.java` 中的 `handleStoreNameClick()` 方法 + +**触发条件**: 连续6次点击视频流界面左上角的店铺名称按钮(`tv_store_name`) +- 点击间隔限制:1秒内点击有效 +- 点击计数:达到6次时触发 +- 用户提示:3次点击后提示剩余次数 + +#### 2.2 门禁不可用弹窗标题点击触发 +**位置**: `GateUnavailableDialog.java` 中的 `handleTitleClick()` 方法 + +**触发条件**: 连续6次点击门禁不可用弹窗的标题(`tvTitle`) +- 点击间隔限制:1秒内点击有效 +- 点击计数:达到6次时触发 +- 日志记录:详细记录点击过程 + +### 3. 密码验证机制 + +**验证逻辑**: +1. 获取用户输入的密码 +2. 从 `LoginDataManager` 获取本地存储的登录密码 +3. 进行字符串比较验证 +4. 验证成功:调用 `unLockAndLeaveFaceDetect()` 方法 +5. 验证失败:显示红色错误提示,清空输入框 + +**安全特性**: +- 密码以密文形式显示 +- 错误时提供明确的用户反馈 +- 支持键盘回车确认 + +### 4. 解锁退出功能 (`unLockAndLeaveFaceDetect()`) + +**执行流程**: +1. 停止摄像头预览 +2. 创建返回登录界面的Intent +3. 设置Intent标志(清除任务栈) +4. 启动MainActivity(登录界面) +5. 结束当前人脸识别界面 +6. 记录详细的操作日志 + +### 5. 摄像头控制集成 + +**自动控制**: +- 弹窗显示时:自动暂停摄像头预览 +- 弹窗关闭时:自动恢复摄像头预览 +- 解锁成功时:永久停止摄像头(准备退出) + +## 界面设计特点 + +### 1. 视觉设计 +- **主题色配色**: 蓝色确认按钮 (#007AFF),灰色取消按钮 +- **圆角设计**: 弹窗、按钮、输入框均采用圆角设计 +- **半透明背景**: 黑色半透明背景 (#CC000000) +- **响应式布局**: 根据屏幕密度自适应大小 + +### 2. 交互设计 +- **焦点管理**: 弹窗显示时自动聚焦到密码输入框 +- **键盘控制**: 自动弹出软键盘,支持回车确认 +- **错误反馈**: 密码错误时显示红色提示文字 +- **防误操作**: 弹窗不可通过点击外部区域关闭 + +### 3. 尺寸规格 +- **弹窗宽度**: 320dp +- **输入框宽度**: 280dp +- **按钮尺寸**: 100dp × 45dp +- **文字大小**: 标题20sp,正文16sp,提示14sp + +## 技术实现细节 + +### 1. 类结构设计 +```java +// 主要类和接口 +- UnlockPasswordDialog: 主弹窗类 +- UnlockDialogListener: 回调接口 +- UnlockPasswordDialogTrigger: 触发器接口 +``` + +### 2. 生命周期管理 +- 创建时初始化所有UI组件 +- 显示时设置焦点和键盘 +- 关闭时清理资源和隐藏键盘 +- 销毁时释放所有引用 + +### 3. 错误处理 +- 完善的异常捕获和日志记录 +- 用户友好的错误提示 +- 安全的资源释放机制 + +## 配置和依赖 + +### 1. 依赖的现有组件 +- `LoginDataManager`: 获取存储的登录密码 +- `LogManager`: 日志记录 +- `CameraPreviewManager`: 摄像头控制 + +### 2. 权限要求 +- 无需额外权限 +- 使用现有的摄像头权限 + +## 使用方法 + +### 1. 普通用户使用 +1. 在人脸识别界面,连续快速点击左上角店铺名称6次 +2. 或者等待门禁不可用弹窗出现时,连续快速点击弹窗标题6次 +3. 在弹出的解锁密码弹窗中输入登录密码 +4. 点击确认或按回车键 +5. 密码正确后自动返回登录界面 + +### 2. 开发者调试 +- 查看日志中的 "解锁" 相关信息 +- 验证点击计数和密码验证逻辑 +- 检查摄像头暂停/恢复状态 + +## 安全性考虑 + +### 1. 访问控制 +- 隐蔽的触发方式(连续6次点击) +- 密码验证机制 +- 无明显的UI提示(防止误操作) + +### 2. 密码安全 +- 使用现有的登录密码验证 +- 密文显示输入内容 +- 错误时不显示具体的密码信息 + +### 3. 日志安全 +- 不记录用户输入的密码 +- 只记录验证成功/失败的结果 +- 详细记录操作流程便于调试 + +## 测试建议 + +### 1. 功能测试 +- 测试两种触发方式 +- 测试正确和错误密码的处理 +- 测试摄像头暂停/恢复功能 +- 测试解锁后的界面跳转 + +### 2. 界面测试 +- 不同屏幕尺寸的适配 +- 横竖屏切换(如支持) +- 软键盘弹出和收起 +- 错误提示的显示和隐藏 + +### 3. 异常测试 +- 密码为空的处理 +- 网络异常时的处理 +- 内存不足时的处理 +- 快速重复操作的处理 + +## 维护说明 + +### 1. 常见问题排查 +- 检查 `LoginDataManager` 中是否有保存的密码 +- 确认点击计数逻辑是否正确 +- 验证摄像头控制广播是否正常 + +### 2. 代码修改建议 +- 若需修改触发次数,修改 `MAX_CLICK_COUNT` 和 `TITLE_MAX_CLICK_COUNT` 常量 +- 若需修改UI样式,在 `createDialog()` 方法中调整 +- 若需修改密码验证逻辑,在 `handleConfirm()` 方法中调整 + +### 3. 日志查看 +- 搜索 "解锁" 关键字查看相关日志 +- 搜索 "点击" 关键字查看触发日志 +- 搜索 "密码" 关键字查看验证日志(不包含实际密码) + +## 总结 + +本实现完全满足了用户的需求: +✅ 两种隐蔽的触发方式(店铺名称和弹窗标题) +✅ 美观简洁的弹窗界面 +✅ 安全的密码验证机制 +✅ 完整的摄像头控制集成 +✅ 可靠的解锁退出功能 +✅ 符合主题色的界面设计 +✅ 完善的错误处理和日志记录 + +该功能为特殊情况下的程序退出提供了安全可靠的解决方案,既保证了系统的安全性,又提供了良好的用户体验。 \ No newline at end of file