受管 WebView 壳:服务端下发 RSA 签名配置,平板按白/黑名单强制跳转管控。面向教室平板、电子班牌的统一上网管理。
| 项 | 值 |
|---|---|
| 包名 / 版本 | edu.campus.browser · versionCode 13 · versionName 0.9.0 |
| 运行环境 | Android 7.0+(minSdk 24)/ targetSdk 34 / 系统 WebView |
| 体积 | release 约 3.77 MB(3,957,557 B;R8 混淆 + 资源压缩,仅 arm64-v8a / armeabi-v7a) |
| 签名 | debug keystore(SHA-256 9E:18:19:8C:...:97:B4),可覆盖安装旧版 |
| 下载 | v0.9.0 Release · CampusBrowser-0.9.0.apk(3,957,557 B) |
| 服务端 | v0.8.0+ 全部功能可用(新字段一律可选);服务端 v0.8.3 起强制校验 UA,因此要求 APP ≥ v0.7.0 —— APP 的所有请求都自带 User-Agent: CampusBrowser/{版本} OkHttp/4.12,/api/v1/public-key 也一样 |
版本对应:v0.9.0 为 APP 界面改版(设计令牌对齐服务端后台 + 加载/错误/拦截/空态四态 + 无障碍 + 大屏适配),服务端无需升级(v0.8.4 即可,接口未变,配置
schema_version仍为 1)。
flowchart TD
A["首次启动"] --> B["填写服务器地址 / 扫码"]
B --> C["注册 + 拉配置 + RSA 验签"]
C -->|失败| B
C -->|成功| D["写入 MMKV 缓存"]
D --> E{"require_startup_photo?"}
E -->|是| F["申请相机 → 前置静默拍照上传"]
E -->|否| G["进入浏览器"]
F --> G
G --> H["WebView 加载首页"]
H --> I{"每次跳转"}
I -->|命中白名单| J["加载"]
I -->|拒绝| K["本地拦截提示页"]
G --> L["按间隔轮询新配置"]
L -->|验签失败/网络异常| M["继续用缓存,绝不覆盖"]
| 模块 | 能力 |
|---|---|
| 受管浏览 | WebView 强制白/黑名单(域名 / host:端口 / 路径前缀,不看协议);禁长按菜单、禁下载、禁缩放,地址栏受管时只读 |
| 原生拦截页 | 越界访问显示原生覆盖层(非 WebView 内联 HTML):标题 + 被拦网址 + 「返回首页 / 重新加载」,系统返回键可退出拦截页 |
| 快捷书签 | 工具栏一键打开;白名单模式下自动放行;长按条目可「添加到桌面」,桌面图标点一下直接进该书签页 |
| 桌面快捷方式 | Android 8+ 用 ShortcutManager.requestPinShortcut,老桌面回退 INSTALL_SHORTCUT 广播;各 ROM 支持不一会有提示;从快捷方式启动跳过拍照门控 |
| 内置 HTML 页 | 服务端托管页 /h/xxx 在书签列表带学校图标标记,可打开、可钉桌面 |
| 扫码 / 唤醒应用 | ZXing 扫码(网址或 app://包名);WebView 自定义 scheme 按服务端白名单拉起,工具栏「应用」按钮快速启动 |
| 启动拍照 | 服务端开关控制;无前置摄像头则上报跳过;拍照/上传失败缓存,下次启动补传 |
| 输错拍照 | 管理密码连续输错静默拍前置照片上传服务端,后台标红告警 |
| 截屏限制 | FLAG_SECURE 全局禁止截屏/录屏(服务端开关) |
| 隐藏入口 | 工具栏 Logo 3 秒内连点 5 次,第 2~5 次有进度圆点反馈;服务端可远程彻底关闭(关闭后状态条提示「管理入口未启用」) |
| 轮询更新 | 按服务端间隔拉取,RSA 验签失败或异常时不覆盖本地缓存 |
| 加载态 / 状态条 | 冷启动全屏加载层(打开门户 → 同步配置 → 核验设备,站内跳转不重显);全局状态条替代瞬时 Toast,3 秒自动淡出、可手动关闭 |
- 颜色令牌:与服务端后台同一套(primary #1E40AF / accent #B45309 / background #F8FAFC / on_surface #0F172A),全部集中在 res/values/colors.xml,布局与 Kotlin 中零裸 hex
- 尺寸令牌:res/values/dimens.xml(间距 4/8/12/16/24/32、圆角 8~14、触摸目标 48dp、字号 28/20/16/15/14/13/12sp);布局里只剩 8 处结构性 0dp,无其他裸 dp
- 触摸目标:工具栏 7 个按钮与隐藏入口均为 48dp 点击区(视觉 24dp / 38dp),地址栏 44dp
- 禁用态:前进/后退走 nav_icon_selector(禁用色 #94A3B8),可用性一眼可辨
- 无障碍:全部图标按钮 contentDescription 走 strings.xml;对比度按 WCAG AA 校验(如地址栏 hint #475569 on #F1F5F9 = 6.92:1);覆盖层弹出时转移焦点并朗读
- 系统栏 / 深色:状态栏与导航栏统一白底深色图标;forceDarkAllowed=false 关掉 OEM 强制反色;横屏刘海屏 shortEdges
- 大屏:values-w600dp 覆盖工具栏 64dp、标题 24sp、配置卡 560dp、PIN 弹窗 400dp
- 隐藏入口输入:6 位数字 PIN(
MD5(pin+盐)发到服务端/api/v1/verify-pin校验,离线统一提示「密码错误」) - 通过后可:修改服务器地址 / 开启临时无管控模式(重启自动恢复)
- 连续输错 5 次锁定 5 分钟
./gradlew assembleDebug # 调试包
./gradlew assembleRelease # 需要 app/keystore.properties + app/keystore/(均不入库)local.properties 需写 sdk.dir=<Android SDK 路径>。工具链:JDK 17 + Gradle 8.9 + AGP 8.5。
后台「系统设置 → 配置签名公钥」复制 PEM,粘贴到 app/src/main/java/edu/campus/browser/SecurityConfig.kt 的 SERVER_PUBLIC_KEY_PEM 后重新打包。
留空则跳过验签(仅调试用);同文件 PIN_SALT 必须与服务端 Services/Security.cs 一致。
app/src/main/java/edu/campus/browser/
├── App.kt Application:MMKV 初始化 + 崩溃捕获
├── SecurityConfig.kt 公钥、PIN 盐、锁定策略
├── crypto/Crypto.kt RSA 验签、PIN 哈希
├── capture/FrontCameraCapture.kt CameraX 前置静默拍照
├── config/AppConfig.kt 配置模型(org.json 解析)
├── config/ConfigRepository.kt 注册/拉取/验签/缓存/轮询/心跳/照片/verify-pin
├── net/UrlRuleMatcher.kt 白/黑名单匹配
├── scan/QrScanner.kt ZXing 扫码封装
├── applaunch/AppLauncher.kt 白名单应用拉起
├── applaunch/BookmarkShortcut.kt 书签钉到桌面
├── applaunch/ShortcutResultReceiver.kt
├── ui/PermissionManager.kt 权限自检与批量申请
├── ui/setup/SetupActivity.kt 首启连接服务器
└── ui/main/
├── MainActivity.kt WebView 主界面、启动门控、轮询、隐藏入口
├── ManagedWebViewClient.kt 跳转拦截与 scheme 处理
└── AdminUnlockDialog.kt 管理密码解锁(输错触发拍照)
| 库 | 用途 |
|---|---|
| Material Components 1.12 | M3 主题与控件 |
| SwipeRefreshLayout 1.1.0 | 下拉刷新 |
| ZXing Android Embedded 4.3.0 | 二维码扫码 |
| CameraX 1.3.4 | 前置静默拍照 |
| OkHttp 4.12 / MMKV 1.3.9 | 网络 / 本地缓存 |
| AndroidX appcompat · constraintlayout · lifecycle | 基础组件 |
GPL-3.0