Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 98 additions & 70 deletions .github/workflows/android_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ name: Android Build

on:
push:
branches: [ main ]
tags: [ "v*" ]
pull_request:
branches: [ main ]
workflow_dispatch:
inputs:
release:
Expand All @@ -11,29 +14,106 @@ on:
default: 'false'
type: boolean

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: write
checks: write
pull-requests: write

jobs:
build:
lint:
runs-on: ubuntu-latest

timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Run detekt
continue-on-error: true
run: ./gradlew detekt
- name: Run Android lint
continue-on-error: true
run: ./gradlew lint
- name: Upload lint reports
if: success() || failure()
uses: actions/upload-artifact@v4
with:
name: lint-reports
path: |
app/build/reports/lint-results-*.html
app/build/reports/detekt/
retention-days: 14

test:
needs: lint
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Run Unit Tests
continue-on-error: true
run: ./gradlew testDebugUnitTest
- name: Upload Test Results
if: success() || failure()
uses: actions/upload-artifact@v4
with:
name: test-results
path: app/build/reports/tests/
retention-days: 14
- name: Publish Test Report
if: success() || failure()
continue-on-error: true
uses: dorny/test-reporter@v1
with:
name: Unit Test Results
path: app/build/test-results/**/TEST-*.xml
reporter: java-junit
fail-on-error: false
- name: Generate Coverage Report
continue-on-error: true
run: ./gradlew koverXmlReportDebug
- name: Upload Coverage Report
if: success() || failure()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: app/build/reports/kover/report.xml
retention-days: 14

build:
needs: test
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Setup signing
env:
KEYSTORE_B64: ${{ secrets.KEYSTORE_BASE64 }}
Expand All @@ -43,7 +123,6 @@ jobs:
run: |
echo "sdk.dir=$ANDROID_HOME" > local.properties
echo "api.url=https://api.openai.com/v1/chat/completions" >> local.properties
echo "api.key=CI_DUMMY_KEY" >> local.properties
echo "api.model=gpt-4" >> local.properties
mkdir -p keystore
echo "$KEYSTORE_B64" | base64 -d > keystore/release.jks
Expand All @@ -53,95 +132,44 @@ jobs:
echo "signing.keyPassword=$KEY_PASS" >> local.properties
- name: Build Debug APK
run: ./gradlew assembleDebug

- name: Build Release APK
run: ./gradlew assembleRelease

- name: Run Unit Tests
run: ./gradlew test

- name: Generate Coverage Report (Kover)
run: ./gradlew koverXmlReportDebug

- name: Upload Debug APK
uses: actions/upload-artifact@v4
with:
name: debug-apk
path: app/build/outputs/apk/debug/*.apk
retention-days: 30

- name: Upload Release APK
uses: actions/upload-artifact@v4
with:
name: release-apk
path: app/build/outputs/apk/release/*.apk
retention-days: 30

- name: Upload Test Coverage Report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: app/build/reports/kover/report.xml
retention-days: 30
retention-days: 90

release:
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.release == 'true')

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
- uses: actions/download-artifact@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Setup signing
env:
KEYSTORE_B64: ${{ secrets.KEYSTORE_BASE64 }}
KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASS: ${{ secrets.KEY_PASSWORD }}
run: |
echo "sdk.dir=$ANDROID_HOME" > local.properties
echo "api.url=https://api.openai.com/v1/chat/completions" >> local.properties
echo "api.key=CI_DUMMY_KEY" >> local.properties
echo "api.model=gpt-4" >> local.properties
mkdir -p keystore
echo "$KEYSTORE_B64" | base64 -d > keystore/release.jks
echo "signing.storeFile=../keystore/release.jks" >> local.properties
echo "signing.storePassword=$KEYSTORE_PASS" >> local.properties
echo "signing.keyAlias=$KEY_ALIAS" >> local.properties
echo "signing.keyPassword=$KEY_PASS" >> local.properties

- name: Build Release APK
run: ./gradlew assembleRelease

name: release-apk
- name: Get version from tag
id: get_version
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT

- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |
app/build/outputs/apk/release/*.apk
files: '*.apk'
name: Release ${{ steps.get_version.outputs.VERSION }}
body: |
## AIAnswerer ${{ steps.get_version.outputs.VERSION }}

### 下载
- APK: Release 版本

### 功能
- 并行答题模式
- 并发数可配置(1-10)
Expand Down
26 changes: 21 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,43 @@

All notable changes to AIAnswerer will be documented in this file.

## [1.7.0] - 2026-07-22
## [1.7.0] - 2026-07-27

### Added
- Three-window floating architecture: Split single floating window into A (Pill), B (Toggles), C (Card) independent windows, each self-contained with natural touch-through in gaps
- Four-window floating architecture: Split single floating window into A (Pill), B (Toggles), C (Status Card), D (Answer Detail) independent windows
- Constants: Added STEALTH_ALPHA, VISIBLE_ALPHA, HIDDEN_ALPHA named constants replacing hardcoded magic numbers
- WindowAContent / WindowBContent / WindowCContent / WindowDContent standalone Composables
- Configurable long-press duration slider in Settings (300ms~3000ms), user-customizable floating button hold time
- Model detection on CTA button: grays out "Configure AI Model First" when no language model is configured

### Fixed
- Notification content now hidden when stealth mode is enabled — shows app name only instead of detailed content
- Stealth mode reading consolidated through AppConfig.isStealthModeEnabled(), eliminating duplicate direct MMKV access
- Window D appearing before content ready: `imageCallbacks.onResult` now sets `showAnswer` AFTER data is populated
- Window C excess blank space: initial height reduced from 200dp to 60dp, matching compact card header size
- Windows persisting after close blocking touch: snapshotFlow observer now destroys C/D when `showAnswer=false`
- Mid-release during long-press triggering scan: `Bouncy` press logic aligned with animation duration, removed 200ms hardcoded commit window
- Card alpha not applied to Window D: added `cardAlpha` parameter propagation
- Removed Success/Error badges (✓/✗) from floating pill
- `removeWindowC()` no longer cascades to destroy Window D; their lifecycles are decoupled
- Window D positioning changed to relative-to-A (instead of relative-to-C), works correctly when C is hidden

### Changed
- FloatingWindowManager fully rewritten for multi-window management: added attachA/B/C, detachA/B/C, per-window updateLayoutA/B/C
- FloatingWindowService heavily refactored for three-window coordination and position synchronization
- FloatingWindowManager fully rewritten for multi-window management: added attachA/B/C/D, detachA/B/C/D, per-window updateLayoutA/B/C/D
- FloatingWindowService heavily refactored for multi-window coordination and position synchronization
- FloatingWindowViewModel: Removed deprecated single-window methods (updateWindowPosition, updateWindowHeight, getCurrentWindowHeightPx, setCurrentWindowHeightPx, updateFloatingWindowHeight)
- RecordingResultCard refactored
- FloatingComponents: Added cardWidthDp (360.dp) as Window C fixed width
- FloatingComponents: Added cardWidthDp (360.dp) as Window C/D fixed width
- Window C/D lifecycle decoupled: C for status messages only, D for answer content only
- Window D height fixed at 400% of Window C height, with vertical scrolling (capped at 560dp)
- Long-press threshold exactly aligned with animation completion progress
- HomePage model check uses `ProviderStorage.getEnabledProvidersFromUserConfigs()` to verify actual model selection
- FloatingWindowManagerTest: Added 669 lines of multi-window operation test coverage

### Removed
- PaginatedAnswerRegressionTest (obsolete after three-window architecture change)
- Deprecated ServiceContext single-window interface methods
- Floating pill Success/Error status badges (✓/✗)

## [1.6.2] - 2026-07-22

Expand Down
79 changes: 51 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,49 +71,72 @@ AI答题助手是一款基于 OCR/视觉模型与大语言模型的安卓答题

```
com.hwb.aianswerer/
├── BaseActivity.kt # 统一语言配置基类
├── MyApplication.kt # Application 初始化
├── MainActivity.kt # 主界面(权限管理、答题设置)
├── FloatingWindowService.kt # 悬浮窗核心服务
├── FloatingWindowManager.kt # 窗口管理(位置/动画
├── BaseActivity.kt # 统一语言配置基类
├── MyApplication.kt # Application 初始化
├── MainActivity.kt # 主界面(权限管理、答题设置)
├── FloatingWindowService.kt # 悬浮窗核心服务(多窗口协调)
├── FloatingWindowManager.kt # 窗口管理(A/B/C/D 独立窗口
├── FloatingWindowViewModel.kt # 悬浮窗状态管理
├── InteractiveTouchLayout.kt # 触摸穿透容器
├── CaptureHandler.kt # 截图→裁剪→识别管线
├── CapturePipeline.kt # 核心识别管线(OCR→VLM→LLM)
├── FloatingAnswerCard.kt # 答案卡片组件
├── ConfirmTextActivity.kt # 识别文本确认/编辑
├── ImageCropActivity.kt # 图片裁剪(四角拖拽)
├── SettingsActivity.kt # 通用设置
├── ModelSettingsActivity.kt # API 模型配置
├── AboutActivity.kt # 关于页面
├── Constants.kt # 常量 · 提示词组装 · 多语言路由中枢
├── SettingsService.kt # 集中式设置读取
├── InteractiveTouchLayout.kt # 触摸穿透容器
├── CaptureHandler.kt # 截图→裁剪→识别管线
├── CapturePipeline.kt # 核心识别管线(OCR→VLM→LLM)
├── RecordingCoordinator.kt # 录制模式协调器
├── ImageCollector.kt # 图片收集器
├── AnswerFetcher.kt # 答案获取器
├── FloatingAnswerCard.kt # 答案卡片组件
├── ConfirmTextActivity.kt # 识别文本确认/编辑
├── ImageCropActivity.kt # 图片裁剪(四角拖拽)
├── SettingsActivity.kt # 通用设置
├── ModelSettingsActivity.kt # API 模型配置
├── AboutActivity.kt # 关于页面
├── Constants.kt # 常量 · 提示词组装 · 多语言路由中枢
├── api/
│ ├── OpenAIClient.kt # OpenAI 兼容 API 客户端
│ ├── OpenAIClient.kt # OpenAI 兼容 API 客户端
│ ├── WebSearchProviders.kt # 多供应商联网搜索
│ └── vision/ # 视觉模型模块
│ └── vision/ # 视觉模型模块
│ ├── VisionProvider.kt
│ ├── VisionProviderFactory.kt
│ ├── VisionFilterResult.kt
│ └── OpenAIVisionProvider.kt
├── config/
│ └── AppConfig.kt # 配置管理(MMKV + 加密存储)
├── models/ # 数据模型
│ ├── AppConfig.kt # 配置管理门面(MMKV + 加密存储)
│ ├── ApiConfig.kt # API 配置
│ ├── UIConfig.kt # UI 配置
│ ├── VisionConfig.kt # 视觉模型配置
│ └── ConfigStorage.kt # MMKV 存储键定义
├── models/ # 数据模型
├── providers/ # 模型厂商管理
│ ├── ProviderStorage.kt
│ ├── ProviderConfigResolver.kt
│ └── ProviderSyncManager.kt
├── ui/
│ ├── components/ # 共享 Compose 组件
│ ├── dialogs/ # 对话框
│ ├── icons/ # 本地图标定义
│ └── theme/ # Material3 主题
│ ├── components/ # 共享 Compose 组件
│ │ ├── PillButtonCard.kt # 悬浮按钮 + 长按手势
│ │ ├── QuickToggles.kt # 快捷开关面板
│ │ ├── FloatingWindowContent.kt # Window A/B/C/D Composable
│ │ ├── FloatingAnswerCard.kt # 答案卡片
│ │ ├── RecordingResultCard.kt # 录制结果卡片
│ │ ├── AnswerCard.kt # 单题答案卡片
│ │ ├── CtaBar.kt # CTA 按钮(模型检测)
│ │ └── ... # 其他组件
│ ├── pages/ # 页面
│ │ ├── HomePage.kt # 主页
│ │ └── SettingsPage.kt # 设置页
│ ├── dialogs/ # 对话框
│ ├── icons/ # 本地图标定义
│ └── theme/ # Material3 主题
└── utils/
├── AppLog.kt # 统一日志工具
├── ClipboardUtil.kt # 剪贴板工具
├── ImageCropUtil.kt # 图片裁剪工具
└── LanguageUtil.kt # 语言切换工具
├── AppLog.kt # 统一日志工具
├── ClipboardUtil.kt # 剪贴板工具
├── ImageCropUtil.kt # 图片裁剪工具
└── LanguageUtil.kt # 语言切换工具
```

### 更新说明

详见 [变更日志](变更日志.md) 或 [CHANGELOG](CHANGELOG.md)。近期重要更新:
- **v1.7.0**: 三窗口悬浮窗架构(A/B/C 独立窗口) + 隐身模式常量提取 + 通知隐身保护 + 废弃接口清理
- **v1.7.0**: 四窗口悬浮窗架构(A/B/C/D 独立窗口)+ C/D 生命周期分离 + 长按时长自定义 + 模型配置检测 + 悬浮窗徽标移除 + 多窗口缺陷修复
- **v1.6.2**: 多厂商测试连接修复 + 悬浮按钮拖拽卡死修复 + 暗色主题响应 + API URL 多版本兼容 + LLM maxTokens 512→4096 + 图标去重 & 主题合规 & 动画可测试性重构 + 答案卡片独立组件

### License
Expand Down
Loading
Loading