diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6be3e9b0..68d92834 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,200 @@
***
+## [v6.79.0] - 2026-06-17
+
+### 🔒 隐私合规 — 修复安卓端自启动问题(androidx.glance.appwidget)
+
+#### 问题描述
+1. 应用商店审核发现:`androidx.glance.appwidget` SDK 在应用退出后触发自启动(1次/秒),无隐私文本覆盖
+2. 审核依据:《个人信息保护法》要求 APP 未向用户明示且未经用户同意,不得存在频繁自启动行为
+3. 根因:`home_widget` 包引入了 `androidx.glance:glance-appwidget:1.1.1` 依赖,项目未使用 Glance Widget 但该库被打包进 APK,其内部 `GlanceAppWidgetReceiver`/`GlanceAppWidgetService` 被系统广播触发导致自启动
+4. 次要问题:8个桌面小部件 Provider 在系统定时 `APPWIDGET_UPDATE` 广播触发时,未检查隐私协议状态即执行数据读取
+
+#### 修复内容
+
+**1. 彻底移除 androidx.glance.appwidget 依赖**
+| 文件 | 变更 |
+|---|---|
+| `packages/home_widget/android/build.gradle` | 移除 `implementation "androidx.glance:glance-appwidget:1.1.1"` 依赖 |
+| `HomeWidgetGlanceWidgetReceiver.kt` | 删除未使用的 Glance Receiver 源文件 |
+| `HomeWidgetGlanceState.kt` | 删除未使用的 Glance State 源文件 |
+
+**2. AndroidManifest 排除 Glance 组件合并**
+| 文件 | 变更 |
+|---|---|
+| `AndroidManifest.xml` | 新增 `tools:node="remove"` 移除 `GlanceAppWidgetReceiver` 和 `GlanceAppWidgetService`,防止残留库通过 manifest merge 注入 |
+
+**3. 所有 Widget Provider 增加隐私协议守门**
+| 文件 | 变更 |
+|---|---|
+| `PrivacyAwareHomeWidgetProvider.kt` | 新建基类,在 `onUpdate` 中检查 `agreement_accepted` 标志,未同意时显示占位视图不读取任何业务数据 |
+| `widget_privacy_placeholder.xml` | 新建隐私占位布局,提示"请先同意隐私政策" |
+| `DailySentenceProvider.kt` | 改为继承 `PrivacyAwareHomeWidgetProvider`,`onUpdate` → `onUpdateWithAgreement` |
+| `ReadlaterProvider.kt` | 同上 |
+| `DailyCardProvider.kt` | 同上 |
+| `FortuneProvider.kt` | 同上 |
+| `CountdownProvider.kt` | 同上 |
+| `PomodoroProvider.kt` | 同上 |
+| `SolarTermProvider.kt` | 同上 |
+| `CheckinProvider.kt` | 同上 |
+| `CtcLatestNoteProvider.kt` | 同上 |
+
+#### 合规影响
+- `androidx.glance.appwidget` 自启动行为彻底消除(依赖移除 + Manifest 排除 + 源文件删除三重防护)
+- Widget Provider 在用户未同意隐私政策前不执行任何数据操作,仅显示占位提示
+- 与现有 `SplashActivity` 协议守门机制形成完整闭环
+
+***
+
+## [v6.78.1] - 2026-06-16
+
+### 🐛 Bug 修复
+
+**鸿蒙端应用商店跳转URL修正 + 外部跳转确认弹窗**
+
+| 文件 | 变更 |
+|---|---|
+| `profile_page.dart` | 鸿蒙应用市场URL从 `C108129465` 修正为 `detail?id=apps.xy.xianyan`;评分跳转前增加 `ExternalLinkDialog` 确认弹窗 |
+| `about_page.dart` | 同上,鸿蒙URL修正 + 外部跳转确认弹窗 |
+
+- 修复鸿蒙端"给个好评"/"评价应用"跳转到错误的应用商店页面
+- 所有平台评分跳转前统一使用 `ExternalLinkDialog` 弹窗提示用户即将离开应用
+
+***
+
+## [v6.78.0] - 2026-06-16
+
+### 🖥️ Windows 桌面端跨平台适配
+
+#### 问题描述
+1. Windows 运行时 `sqlite3.dll` 未找到,数据库无法初始化
+2. `home_widget` 插件无 Windows 实现,抛出 MissingPluginException
+3. `LiquidGlassLayer` 需要 Impeller 渲染引擎,Windows 不支持导致大量警告
+4. BotToast `markInitialized()` 在每次 widget rebuild 时重复打印日志
+5. Windows 标题栏不跟随应用深色主题
+6. Windows 应用使用默认 Flutter 图标
+
+#### 修复内容
+
+**1. sqlite3.dll 加载修复**
+| 文件 | 变更 |
+|---|---|
+| `windows/CMakeLists.txt` | 新增 install 规则,将预编译 sqlite3.dll 复制到构建输出目录 |
+
+**2. home_widget 跨平台兼容**
+| 文件 | 变更 |
+|---|---|
+| `home_widget_service.dart` | 新增 `_isPlatformSupported` 检查,Windows/Linux 桌面端跳过所有 HomeWidget 调用 |
+
+**3. LiquidGlass 平台能力检测**
+| 文件 | 变更 |
+|---|---|
+| `platform_capability.dart` | `liquidGlass` 能力注册改为仅 iOS/Android/macOS |
+| `app.dart` | 标准端路径根据 `PlatformCapabilities.supports(CapabilityKey.liquidGlass)` 决定是否使用 GlassTheme |
+
+**4. BotToast 重复初始化修复**
+| 文件 | 变更 |
+|---|---|
+| `app_toast.dart` | `markInitialized()` 添加去重检查,避免重复打印日志 |
+
+**5. Windows 标题栏深色主题**
+| 文件 | 变更 |
+|---|---|
+| `win32_window.h/cpp` | 新增 `SetDarkMode(HWND, bool)` 方法 |
+| `flutter_window.cpp` | 注册 `MethodChannel("com.xianyan.windows")` 接收 Flutter 端主题切换 |
+| `windows_platform_service.dart` | 新增 Windows 平台服务,`syncTheme(bool)` 通知原生层 |
+| `app.dart` | 调用 `WindowsPlatformService.syncTheme()` 同步标题栏主题 |
+
+**6. Windows 应用图标替换**
+| 文件 | 变更 |
+|---|---|
+| `windows/runner/resources/app_icon.ico` | 使用项目图标替换默认 Flutter 图标 |
+
+***
+
+## [v6.77.0] - 2026-06-16
+
+### 🔒 隐私合规 — 移除自动剪贴板监听 + 协议前权限使用增强
+
+#### 问题描述
+1. 安卓端未同意隐私政策前,`ClipboardSyncService` 以2秒定时轮询读取剪贴板,违反合规要求
+2. 多处 UI 代码直接调用 `Clipboard.getData`,绕过隐私协议检查
+3. `HapticService.init()` 在协议前调用 `Vibrate.canVibrate`,触发原生插件
+4. `ClipboardMonitorService` 在用户进入稍后读页面时自动检查剪贴板,用户无感知
+
+#### 修复内容
+
+**1. ClipboardBridge 统一剪贴板入口(核心修复)**
+| 变更 | 说明 |
+|---|---|
+| `clipboard_bridge.dart` | 新增隐私协议守卫,未同意协议时 `getData()` 返回 null,`hasStrings()` 返回 false |
+| 所有 `Clipboard.getData` 调用点 | 统一替换为 `ClipboardBridge.getData()`,共7个文件 |
+
+**2. 移除自动剪贴板监听,改为用户点击触发**
+| 变更 | 说明 |
+|---|---|
+| `readlater_page.dart` | 移除 `initState` 中的 `checkClipboardOnce()` 调用 |
+| `chat_flow_readlater_sync_helper.dart` | 启用监控按钮点击后立即检查一次剪贴板 |
+
+**3. 剪贴板服务统一入口**
+| 文件 | 变更 |
+|---|---|
+| `clipboard_monitor_service.dart` | 使用 `ClipboardBridge.getData()` 替代 `Clipboard.getData` |
+| `clipboard_sync_service.dart` | 使用 `ClipboardBridge.getData()` 替代 `Clipboard.getData` |
+| `clipboard_manager_service.dart` | `syncNow()` 使用 `ClipboardBridge.getData()` 替代 `Clipboard.getData` |
+
+**4. UI层剪贴板调用统一**
+| 文件 | 变更 |
+|---|---|
+| `ctc_note_edit_page.dart` | 粘贴功能使用 `ClipboardBridge.getData()` |
+| `ctc_settings_page.dart` | 从剪贴板导入使用 `ClipboardBridge.getData()` |
+| `link_input_sheet.dart` | 从剪贴板粘贴链接使用 `ClipboardBridge.getData()` |
+| `chat_flow_readlater_sync_helper.dart` | 查看剪贴板使用 `ClipboardBridge.getData()` |
+| `leisure_import_dialog.dart` | 读取剪贴板使用 `ClipboardBridge.getData()` |
+| `clipboard_flow_page.dart` | 粘贴同步使用 `ClipboardBridge.getData()` |
+
+**5. 触觉反馈服务延迟初始化**
+| 变更 | 说明 |
+|---|---|
+| `main.dart` | 移除 `HapticService.init()` 调用 |
+| `post_agreement_initializer.dart` | 新增 `HapticService.init()` 调用,协议同意后才初始化 |
+| `haptic_service.dart` | 所有公开方法增加 `_agreementAccepted` 守卫,未同意协议时不执行震动 |
+
+**6. 原生端敏感插件列表扩展**
+| 变更 | 说明 |
+|---|---|
+| `MainActivity.kt` | `SENSITIVE_PLUGIN_CLASSES` 新增3个插件:`NetworkInfoPlugin`(网络信息)、`AudioplayersPlugin`(音频播放)、`FlutterVibratePlugin`(震动) |
+
+**7. 快捷方式修复**
+| 变更 | 说明 |
+|---|---|
+| `MainActivity.kt` | 修复 `shortcutManager.shortcuts` 编译错误,改用 `setDynamicShortcuts()` |
+| `MainActivity.kt` | `handleShortcutIntent` 增加对 `android:data` URI 解析,支持 `xianyan://shortcut/action_xxx` 协议 |
+| `MainActivity.kt` | 快捷方式图标改用 `R.drawable.ic_shortcut_theme` 和 `R.drawable.ic_shortcut_search` 矢量图标 |
+| `shortcuts.xml` | 添加静态快捷方式定义,包含图标和 intent 配置 |
+
+#### 修改文件
+- `lib/core/utils/platform/clipboard_bridge.dart`
+- `lib/core/services/clipboard_monitor_service.dart`
+- `lib/features/file_transfer/services/clipboard_sync_service.dart`
+- `lib/features/file_transfer/collaboration/clipboard/clipboard_manager_service.dart`
+- `lib/features/file_transfer/collaboration/clipboard/clipboard_flow_page.dart`
+- `lib/features/ctc/presentation/pages/ctc_note_edit_page.dart`
+- `lib/features/ctc/presentation/pages/ctc_settings_page.dart`
+- `lib/features/discover/presentation/widgets/chat_input/link_input_sheet.dart`
+- `lib/features/discover/presentation/widgets/chat/chat_flow_readlater_sync_helper.dart`
+- `lib/features/tool_center/leisure/presentation/pages/leisure_import_dialog.dart`
+- `lib/core/services/device/haptic_service.dart`
+- `lib/core/services/post_agreement_initializer.dart`
+- `lib/main.dart`
+- `lib/features/home/presentation/providers/readlater_page.dart`
+- `android/app/src/main/kotlin/apps/xy/xianyan/MainActivity.kt`
+- `android/app/src/main/kotlin/apps/xy/xianyan/SplashActivity.kt`
+- `android/app/src/main/res/xml/shortcuts.xml`
+
+***
+
## [v6.76.0] - 2026-06-15
### 🔧 多项UI修复与功能增强
diff --git a/Scripts/package_msix.ps1 b/Scripts/package_msix.ps1
new file mode 100644
index 00000000..dece00b5
--- /dev/null
+++ b/Scripts/package_msix.ps1
@@ -0,0 +1,118 @@
+# Xianyan MSIX Packaging Script
+# Auto-syncs version from pubspec.yaml to msix_version
+#
+# Usage:
+# .\scripts\package_msix.ps1 # Store submission
+# .\scripts\package_msix.ps1 -LocalTest # Local test (sign + install)
+
+param(
+ [switch]$LocalTest = $false
+)
+
+$ErrorActionPreference = "Stop"
+$ProjectRoot = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
+$PubspecPath = Join-Path $ProjectRoot "pubspec.yaml"
+
+Write-Host "========================================" -ForegroundColor Cyan
+Write-Host " Xianyan MSIX Packaging Tool" -ForegroundColor Cyan
+Write-Host "========================================" -ForegroundColor Cyan
+
+# 1. Read version from pubspec.yaml
+$pubspecContent = Get-Content $PubspecPath -Raw -Encoding UTF8
+if ($pubspecContent -match 'version:\s*(\d+)\.(\d+)\.(\d+)\+(\d+)') {
+ $major = $Matches[1]
+ $minor = $Matches[2]
+ $patch = $Matches[3]
+ $build = $Matches[4]
+ $flutterVersion = "${major}.${minor}.${patch}+${build}"
+ $msixVersion = "${major}.${minor}.${patch}.0"
+ Write-Host "[1/5] Version: $flutterVersion -> MSIX: $msixVersion" -ForegroundColor Green
+} else {
+ Write-Host "[1/5] ERROR: Cannot read version from pubspec.yaml" -ForegroundColor Red
+ exit 1
+}
+
+# 2. Sync msix_version in pubspec.yaml
+$updatedContent = $pubspecContent -replace 'msix_version:\s*\d+\.\d+\.\d+\.\d+', "msix_version: $msixVersion"
+[System.IO.File]::WriteAllText($PubspecPath, $updatedContent, [System.Text.UTF8Encoding]::new($false))
+Write-Host "[2/5] msix_version synced: $msixVersion" -ForegroundColor Green
+
+# 3. Set store mode
+if ($LocalTest) {
+ $updatedContent = $updatedContent -replace 'store:\s*true', 'store: false'
+ [System.IO.File]::WriteAllText($PubspecPath, $updatedContent, [System.Text.UTF8Encoding]::new($false))
+ Write-Host "[3/5] Mode: Local Test (store: false)" -ForegroundColor Yellow
+} else {
+ $updatedContent = $updatedContent -replace 'store:\s*false', 'store: true'
+ [System.IO.File]::WriteAllText($PubspecPath, $updatedContent, [System.Text.UTF8Encoding]::new($false))
+ Write-Host "[3/5] Mode: Store Submit (store: true)" -ForegroundColor Green
+}
+
+# 4. Build MSIX
+Set-Location $ProjectRoot
+Write-Host "[4/5] Building MSIX..." -ForegroundColor Cyan
+dart run msix:create 2>&1 | ForEach-Object { Write-Host $_ }
+
+$msixPath = Join-Path $ProjectRoot "build\windows\x64\runner\Release\xianyan.msix"
+if (-not (Test-Path $msixPath)) {
+ Write-Host "[4/5] ERROR: MSIX file not generated" -ForegroundColor Red
+ exit 1
+}
+$msixSize = [math]::Round((Get-Item $msixPath).Length / 1MB, 2)
+Write-Host "[4/5] MSIX generated: $msixPath ($msixSize MB)" -ForegroundColor Green
+
+# 5. Local test: sign + install
+if ($LocalTest) {
+ $certPath = Join-Path $ProjectRoot "build\cert\test_certificate.pfx"
+ $cerPath = Join-Path $ProjectRoot "build\cert\test.cer"
+ $signtoolPath = "d:\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe"
+
+ if (-not (Test-Path $certPath)) {
+ Write-Host "[5/5] Generating test certificate..." -ForegroundColor Cyan
+ New-Item -ItemType Directory -Force -Path (Split-Path $certPath) | Out-Null
+ $cert = New-SelfSignedCertificate -Type Custom `
+ -Subject "CN=0334AD95-A5D7-4597-B71F-AA0696B7E9F7" `
+ -KeyUsage DigitalSignature `
+ -FriendlyName "Xianyan Test Cert" `
+ -CertStoreLocation "Cert:\CurrentUser\My" `
+ -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}")
+ Export-PfxCertificate -Cert $cert -FilePath $certPath `
+ -Password (ConvertTo-SecureString -String "123456" -Force -AsPlainText) | Out-Null
+ Export-Certificate -Cert $cert -FilePath $cerPath | Out-Null
+ Write-Host " Certificate generated" -ForegroundColor Green
+ }
+
+ if (Test-Path $signtoolPath) {
+ Write-Host "[5/5] Signing MSIX..." -ForegroundColor Cyan
+ & $signtoolPath sign /fd SHA256 /f $certPath /p "123456" $msixPath 2>&1 | ForEach-Object { Write-Host $_ }
+ Write-Host " Signed successfully" -ForegroundColor Green
+ } else {
+ Write-Host "[5/5] WARNING: signtool.exe not found, skip signing" -ForegroundColor Yellow
+ }
+
+ if (Test-Path $cerPath) {
+ Write-Host "[5/5] Installing certificate to Trusted Root (requires admin)..." -ForegroundColor Cyan
+ Start-Process powershell -Verb RunAs -Wait -ArgumentList @(
+ '-Command',
+ "Import-Certificate -FilePath '$cerPath' -CertStoreLocation 'Cert:\LocalMachine\Root'; Start-Sleep 2"
+ )
+ }
+
+ Write-Host "[5/5] Installing MSIX..." -ForegroundColor Cyan
+ Add-AppxPackage -Path $msixPath -ForceUpdateFromAnyVersion 2>&1 | ForEach-Object { Write-Host $_ }
+ Write-Host " Installed! Search 'xianyan' in Start Menu" -ForegroundColor Green
+
+ # Restore store: true
+ $finalContent = [System.IO.File]::ReadAllText($PubspecPath, [System.Text.UTF8Encoding]::new($false))
+ $finalContent = $finalContent -replace 'store:\s*false', 'store: true'
+ [System.IO.File]::WriteAllText($PubspecPath, $finalContent, [System.Text.UTF8Encoding]::new($false))
+ Write-Host " Restored store: true" -ForegroundColor DarkGray
+} else {
+ Write-Host "[5/5] Store package ready. Upload to Partner Center:" -ForegroundColor Green
+ Write-Host " $msixPath" -ForegroundColor White
+}
+
+Write-Host ""
+Write-Host "========================================" -ForegroundColor Cyan
+Write-Host " Done!" -ForegroundColor Cyan
+Write-Host "========================================" -ForegroundColor Cyan
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 105f30af..6c4fba67 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -255,6 +255,15 @@
android:name="androidx.work.impl.background.systemalarm.RescheduleReceiver"
tools:node="remove" />
+
+
+
+
+
pendingManageSpaceAction = null
window.decorView.post { invokeFlutterMethod(action) }
}
pendingShortcutAction?.let { action ->
pendingShortcutAction = null
+ Log.i(TAG, "MainActivity.onResume: 处理快捷方式 $action")
window.decorView.post { invokeShortcutAction(action) }
}
}
@@ -246,6 +281,84 @@ class MainActivity : FlutterActivity() {
}
}
}
+
+ // ---- ShortcutManager MethodChannel(快捷方式创建和图标设置)----
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
+ MethodChannel(
+ flutterEngine.dartExecutor.binaryMessenger,
+ SHORTCUT_MANAGER_CHANNEL
+ ).setMethodCallHandler { call, result ->
+ when (call.method) {
+ "createShortcuts" -> {
+ // 从Flutter端调用,创建桌面快捷方式
+ try {
+ createShortcuts()
+ result.success(true)
+ Log.i(TAG, "ShortcutManager: 快捷方式创建成功")
+ } catch (e: Exception) {
+ result.error("SHORTCUT_ERROR", "创建快捷方式失败: ${e.message}", null)
+ Log.e(TAG, "ShortcutManager: 创建快捷方式失败", e)
+ }
+ }
+ else -> result.notImplemented()
+ }
+ }
+ Log.i(TAG, "ShortcutManager: MethodChannel已注册")
+ } else {
+ Log.i(TAG, "ShortcutManager: API版本不支持(${Build.VERSION.SDK_INT}),跳过")
+ }
+ }
+
+ /**
+ * 使用ShortcutManager API创建桌面快捷方式
+ * API 25+ 支持,使用应用图标作为快捷方式图标
+ */
+ private fun createShortcuts() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
+ Log.w(TAG, "ShortcutManager: API < 25,不支持动态快捷方式")
+ return
+ }
+
+ val shortcutManager = getSystemService(ShortcutManager::class.java)
+ if (!shortcutManager.isRequestPinShortcutSupported) {
+ Log.w(TAG, "ShortcutManager: 设备不支持快捷方式固定")
+ return
+ }
+
+ val themeIntent = Intent(this, MainActivity::class.java).apply {
+ action = Intent.ACTION_RUN
+ putExtra(EXTRA_SHORTCUT_ACTION, "action_theme")
+ addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
+ }
+
+ val searchIntent = Intent(this, MainActivity::class.java).apply {
+ action = Intent.ACTION_RUN
+ putExtra(EXTRA_SHORTCUT_ACTION, "action_search")
+ addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
+ }
+
+ // 使用drawable资源作为快捷方式图标(矢量图标)
+ val themeIcon = Icon.createWithResource(this, R.drawable.ic_shortcut_theme)
+ val searchIcon = Icon.createWithResource(this, R.drawable.ic_shortcut_search)
+
+ val shortcuts = listOf(
+ ShortcutInfo.Builder(this, "action_theme")
+ .setShortLabel("主题个性化")
+ .setLongLabel("打开主题个性化设置")
+ .setIcon(themeIcon)
+ .setIntent(themeIntent)
+ .build(),
+ ShortcutInfo.Builder(this, "action_search")
+ .setShortLabel("搜索功能")
+ .setLongLabel("搜索APP内功能")
+ .setIcon(searchIcon)
+ .setIntent(searchIntent)
+ .build()
+ )
+
+ // 使用setDynamicShortcuts设置动态快捷方式
+ shortcutManager.setDynamicShortcuts(shortcuts)
+ Log.i(TAG, "ShortcutManager: 已设置 ${shortcuts.size} 个动态快捷方式")
}
override fun onDestroy() {
@@ -290,13 +403,26 @@ class MainActivity : FlutterActivity() {
/**
* 处理桌面快捷方式Intent
- * 检测intent中是否包含shortcut_action extra,如果有则通过MethodChannel通知Flutter端
+ * 检测intent中是否包含shortcut_action extra或xianyan://协议,
+ * 如果有则通过MethodChannel通知Flutter端
*/
private fun handleShortcutIntent(intent: Intent?) {
if (intent == null) return
- // 从intent extras中读取shortcut_action
- val shortcutAction = intent.getStringExtra(EXTRA_SHORTCUT_ACTION)
+ // 优先从intent extras中读取shortcut_action(动态快捷方式)
+ var shortcutAction = intent.getStringExtra(EXTRA_SHORTCUT_ACTION)
+
+ // 如果没有extra,检查intent.data(静态快捷方式使用xianyan://协议)
+ if (shortcutAction == null && intent.data != null) {
+ val uri = intent.data.toString()
+ Log.i(TAG, "handleShortcutIntent: 检查intent.data: $uri")
+ // xianyan://shortcut/action_theme -> 提取 action_theme
+ if (uri.startsWith("xianyan://shortcut/")) {
+ shortcutAction = uri.removePrefix("xianyan://shortcut/")
+ Log.i(TAG, "handleShortcutIntent: 从data URI提取shortcutAction: $shortcutAction")
+ }
+ }
+
if (shortcutAction != null) {
Log.i(TAG, "handleShortcutIntent: 收到快捷方式action: $shortcutAction")
pendingShortcutAction = shortcutAction
diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/SplashActivity.kt b/android/app/src/main/kotlin/apps/xy/xianyan/SplashActivity.kt
index 678dac69..edaaeda6 100644
--- a/android/app/src/main/kotlin/apps/xy/xianyan/SplashActivity.kt
+++ b/android/app/src/main/kotlin/apps/xy/xianyan/SplashActivity.kt
@@ -1,10 +1,11 @@
// ============================================================
// 闲言APP — 启动页Activity(隐私协议守门人)
// 创建时间: 2026-06-10
-// 更新时间: 2026-06-13
+// 更新时间: 2026-06-16
// 作用: 应用启动入口,在用户同意隐私政策前阻止Flutter引擎启动
// 从根本上防止SensorsPlugin等敏感插件在协议前读取传感器
-// 上次更新: 修复overridePendingTransition弃用警告,兼容Android 14+新API
+// 上次更新: 支持快捷方式action转发,确保快捷方式通过SplashActivity
+// 不绕过协议守门,同时正确传递shortcut_action到MainActivity
// ============================================================
// 设计说明:
// 此Activity是应用的LAUNCHER入口,在AndroidManifest中声明。
@@ -16,6 +17,11 @@
// 5. 用户同意 → 持久化状态,启动MainActivity
// 6. 用户拒绝 → 退出应用
//
+// 快捷方式流程:
+// shortcuts.xml中targetClass指向SplashActivity
+// SplashActivity读取shortcut_action extra并转发给MainActivity
+// 确保协议守门不被绕过
+//
// 关键:MainActivity(Flutter引擎)只有在用户同意后才被启动,
// 因此GeneratedPluginRegistrant.registerWith()中的SensorsPlugin
// 等敏感插件不会在协议前被注册和触发onAttachedToActivity。
@@ -24,21 +30,26 @@
package apps.xy.xianyan
import android.content.Intent
-import android.net.Uri
+import android.content.res.Configuration
import android.os.Bundle
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.edit
+import androidx.core.graphics.toColorInt
+import androidx.core.net.toUri
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.io.File
+@Suppress(
+ "unused", // TAG 常量保留供日志使用
+ "DEPRECATION" // overridePendingTransition 已有兼容处理
+)
class SplashActivity : AppCompatActivity() {
companion object {
- private const val TAG = "SplashActivity"
-
// 原生SharedPreferences — 与MainActivity共用
private const val PREFS_NAME = "xianyan_prefs"
private const val KEY_AGREEMENT_ACCEPTED = "agreement_accepted"
@@ -54,16 +65,14 @@ class SplashActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
- val hasAgreed = prefs.getBoolean(KEY_AGREEMENT_ACCEPTED, false)
-
when {
// 已同意协议 → 直接进入Flutter
- hasAgreed -> {
+ prefs.getBoolean(KEY_AGREEMENT_ACCEPTED, false) -> {
startMainActivity()
}
// 老用户升级(有Flutter数据但未设置原生协议标志)→ 自动迁移
isExistingUser() -> {
- prefs.edit().putBoolean(KEY_AGREEMENT_ACCEPTED, true).apply()
+ prefs.edit { putBoolean(KEY_AGREEMENT_ACCEPTED, true) }
startMainActivity()
}
// 新用户 → 显示隐私协议对话框
@@ -73,6 +82,16 @@ class SplashActivity : AppCompatActivity() {
}
}
+ // ---- 快捷方式action读取 ----
+
+ /**
+ * 从Intent中读取快捷方式action
+ * shortcuts.xml中的shortcut通过extra传递action
+ */
+ private fun getShortcutAction(): String? {
+ return intent?.getStringExtra("shortcut_action")
+ }
+
// ---- 老用户检测 ----
/**
@@ -162,7 +181,9 @@ class SplashActivity : AppCompatActivity() {
"• 🎤 麦克风 — 用于语音输入和录音\n" +
"• 👆 生物识别 — 用于应用锁和隐私保护\n" +
"• 📡 WiFi — 用于局域网文件传输\n" +
- "• 🎵 音频/视频 — 用于媒体播放和编辑"
+ "• 🎵 音频/视频 — 用于媒体播放和编辑\n" +
+ "• 📋 剪贴板 — 用于用户主动粘贴/分享内容(仅用户操作时读取)\n" +
+ "• 📐 传感器 — 用于摇一摇等交互功能(仅用户触发时使用)"
}
// ---- 协议同意/拒绝处理 ----
@@ -173,9 +194,7 @@ class SplashActivity : AppCompatActivity() {
*/
private fun onAgreementAccepted() {
getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
- .edit()
- .putBoolean(KEY_AGREEMENT_ACCEPTED, true)
- .apply()
+ .edit { putBoolean(KEY_AGREEMENT_ACCEPTED, true) }
startMainActivity()
}
@@ -192,9 +211,17 @@ class SplashActivity : AppCompatActivity() {
/**
* 启动Flutter主Activity
* 使用无动画过渡,保持启动页视觉连续性
+ * 同时转发快捷方式action(如果有)
+ * 如果MainActivity已在运行(热启动),复用现有实例并传递action
*/
private fun startMainActivity() {
val intent = Intent(this, MainActivity::class.java)
+ // 转发快捷方式action到MainActivity
+ getShortcutAction()?.let { action ->
+ intent.putExtra("shortcut_action", action)
+ }
+ // 如果MainActivity已在任务栈中,复用实例而非创建新实例
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(intent)
overrideTransitionCompat(0, 0)
finish()
@@ -218,12 +245,13 @@ class SplashActivity : AppCompatActivity() {
/**
* 在浏览器中打开URL
+ * 使用 createChooser 确保走外部浏览器,避免匹配应用自身的 Deep Link
*/
private fun openUrl(url: String) {
try {
- val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
- startActivity(intent)
- } catch (e: Exception) {
+ val intent = Intent(Intent.ACTION_VIEW, url.toUri())
+ startActivity(Intent.createChooser(intent, null))
+ } catch (_: Exception) {
// 无浏览器可用,忽略
}
}
@@ -231,16 +259,17 @@ class SplashActivity : AppCompatActivity() {
/**
* 暗色模式适配:调整自定义布局中的文字颜色
*/
+ @Suppress("NewApi") // minSdk=28,实际不会触发
private fun adaptDarkMode(root: View) {
val isDark = resources.configuration.uiMode and
- android.content.res.Configuration.UI_MODE_NIGHT_MASK ==
- android.content.res.Configuration.UI_MODE_NIGHT_YES
+ Configuration.UI_MODE_NIGHT_MASK ==
+ Configuration.UI_MODE_NIGHT_YES
if (!isDark) return
- val titleColor = android.graphics.Color.parseColor("#E0E0E0")
- val bodyColor = android.graphics.Color.parseColor("#B0B0B0")
- val linkColor = android.graphics.Color.parseColor("#90CAF9")
+ val titleColor = "#E0E0E0".toColorInt()
+ val bodyColor = "#B0B0B0".toColorInt()
+ val linkColor = "#90CAF9".toColorInt()
adjustColors(root, titleColor, bodyColor, linkColor)
}
diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/widget/CheckinProvider.kt b/android/app/src/main/kotlin/apps/xy/xianyan/widget/CheckinProvider.kt
index 5599f1be..48da8854 100644
--- a/android/app/src/main/kotlin/apps/xy/xianyan/widget/CheckinProvider.kt
+++ b/android/app/src/main/kotlin/apps/xy/xianyan/widget/CheckinProvider.kt
@@ -1,10 +1,10 @@
/**
* CheckinProvider.kt
* 创建时间: 2026-05-19
- * 更新时间: 2026-05-19
+ * 更新时间: 2026-06-17
* 名称: 每日签到桌面小部件Provider
* 作用: 在Android桌面展示连续签到天数和快捷签到入口,支持深色主题
- * 上次更新内容: 初始创建,支持light/dark双主题布局
+ * 上次更新内容: 改为继承PrivacyAwareHomeWidgetProvider,未同意隐私政策时不执行更新
*/
package apps.xy.xianyan.widget
@@ -15,10 +15,9 @@ import android.widget.RemoteViews
import apps.xy.xianyan.R
import apps.xy.xianyan.MainActivity
import es.antonborri.home_widget.HomeWidgetLaunchIntent
-import es.antonborri.home_widget.HomeWidgetProvider
-class CheckinProvider : HomeWidgetProvider() {
- override fun onUpdate(
+class CheckinProvider : PrivacyAwareHomeWidgetProvider() {
+ override fun onUpdateWithAgreement(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/widget/CountdownProvider.kt b/android/app/src/main/kotlin/apps/xy/xianyan/widget/CountdownProvider.kt
index 9a330101..e6101a45 100644
--- a/android/app/src/main/kotlin/apps/xy/xianyan/widget/CountdownProvider.kt
+++ b/android/app/src/main/kotlin/apps/xy/xianyan/widget/CountdownProvider.kt
@@ -1,10 +1,10 @@
/**
* CountdownProvider.kt
* 创建时间: 2026-05-19
- * 更新时间: 2026-05-19
+ * 更新时间: 2026-06-17
* 名称: 倒计时桌面小部件Provider
* 作用: 在Android桌面展示自定义倒计时事件天数,支持深色主题
- * 上次更新内容: 初始创建,支持light/dark双主题布局
+ * 上次更新内容: 改为继承PrivacyAwareHomeWidgetProvider,未同意隐私政策时不执行更新
*/
package apps.xy.xianyan.widget
@@ -15,12 +15,11 @@ import android.widget.RemoteViews
import apps.xy.xianyan.R
import apps.xy.xianyan.MainActivity
import es.antonborri.home_widget.HomeWidgetLaunchIntent
-import es.antonborri.home_widget.HomeWidgetProvider
import java.time.LocalDate
import java.time.temporal.ChronoUnit
-class CountdownProvider : HomeWidgetProvider() {
- override fun onUpdate(
+class CountdownProvider : PrivacyAwareHomeWidgetProvider() {
+ override fun onUpdateWithAgreement(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/widget/CtcLatestNoteProvider.kt b/android/app/src/main/kotlin/apps/xy/xianyan/widget/CtcLatestNoteProvider.kt
index f5cba66c..5691466e 100644
--- a/android/app/src/main/kotlin/apps/xy/xianyan/widget/CtcLatestNoteProvider.kt
+++ b/android/app/src/main/kotlin/apps/xy/xianyan/widget/CtcLatestNoteProvider.kt
@@ -1,10 +1,10 @@
/**
* CtcLatestNoteProvider.kt
* 创建时间: 2026-06-15
- * 更新时间: 2026-06-15
+ * 更新时间: 2026-06-17
* 名称: CTC最新笔记桌面小部件Provider
* 作用: 在Android桌面展示CTC最新笔记的钥匙名和内容预览,支持深色主题
- * 上次更新内容: 初始创建,支持light/dark双主题布局,点击跳转/ctc路由
+ * 上次更新内容: 改为继承PrivacyAwareHomeWidgetProvider,未同意隐私政策时不执行更新
*/
package apps.xy.xianyan.widget
@@ -15,10 +15,9 @@ import android.widget.RemoteViews
import apps.xy.xianyan.R
import apps.xy.xianyan.MainActivity
import es.antonborri.home_widget.HomeWidgetLaunchIntent
-import es.antonborri.home_widget.HomeWidgetProvider
-class CtcLatestNoteProvider : HomeWidgetProvider() {
- override fun onUpdate(
+class CtcLatestNoteProvider : PrivacyAwareHomeWidgetProvider() {
+ override fun onUpdateWithAgreement(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/widget/DailyCardProvider.kt b/android/app/src/main/kotlin/apps/xy/xianyan/widget/DailyCardProvider.kt
index 85d339e1..8ef6898a 100644
--- a/android/app/src/main/kotlin/apps/xy/xianyan/widget/DailyCardProvider.kt
+++ b/android/app/src/main/kotlin/apps/xy/xianyan/widget/DailyCardProvider.kt
@@ -1,10 +1,10 @@
/**
* DailyCardProvider.kt
* 创建时间: 2026-05-19
- * 更新时间: 2026-05-19
+ * 更新时间: 2026-06-17
* 名称: 日签卡片桌面小部件Provider
* 作用: 在Android桌面展示精美日签卡片,含日期、句子和作者,支持深色主题
- * 上次更新内容: 新增dark主题支持,根据widget_theme_mode切换布局
+ * 上次更新内容: 改为继承PrivacyAwareHomeWidgetProvider,未同意隐私政策时不执行更新
*/
package apps.xy.xianyan.widget
@@ -15,10 +15,9 @@ import android.widget.RemoteViews
import apps.xy.xianyan.R
import apps.xy.xianyan.MainActivity
import es.antonborri.home_widget.HomeWidgetLaunchIntent
-import es.antonborri.home_widget.HomeWidgetProvider
-class DailyCardProvider : HomeWidgetProvider() {
- override fun onUpdate(
+class DailyCardProvider : PrivacyAwareHomeWidgetProvider() {
+ override fun onUpdateWithAgreement(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/widget/DailySentenceProvider.kt b/android/app/src/main/kotlin/apps/xy/xianyan/widget/DailySentenceProvider.kt
index c0f5cc51..5e9bd29b 100644
--- a/android/app/src/main/kotlin/apps/xy/xianyan/widget/DailySentenceProvider.kt
+++ b/android/app/src/main/kotlin/apps/xy/xianyan/widget/DailySentenceProvider.kt
@@ -1,10 +1,10 @@
/**
* DailySentenceProvider.kt
* 创建时间: 2026-05-19
- * 更新时间: 2026-05-19
+ * 更新时间: 2026-06-17
* 名称: 每日一句桌面小部件Provider
* 作用: 在Android桌面展示每日精选句子和作者,支持深色主题
- * 上次更新内容: 新增dark主题支持,根据widget_theme_mode切换布局
+ * 上次更新内容: 改为继承PrivacyAwareHomeWidgetProvider,未同意隐私政策时不执行更新
*/
package apps.xy.xianyan.widget
@@ -15,10 +15,9 @@ import android.widget.RemoteViews
import apps.xy.xianyan.R
import apps.xy.xianyan.MainActivity
import es.antonborri.home_widget.HomeWidgetLaunchIntent
-import es.antonborri.home_widget.HomeWidgetProvider
-class DailySentenceProvider : HomeWidgetProvider() {
- override fun onUpdate(
+class DailySentenceProvider : PrivacyAwareHomeWidgetProvider() {
+ override fun onUpdateWithAgreement(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/widget/FortuneProvider.kt b/android/app/src/main/kotlin/apps/xy/xianyan/widget/FortuneProvider.kt
index 5c330fae..dfcb0d81 100644
--- a/android/app/src/main/kotlin/apps/xy/xianyan/widget/FortuneProvider.kt
+++ b/android/app/src/main/kotlin/apps/xy/xianyan/widget/FortuneProvider.kt
@@ -1,10 +1,10 @@
/**
* FortuneProvider.kt
* 创建时间: 2026-05-19
- * 更新时间: 2026-05-19
+ * 更新时间: 2026-06-17
* 名称: 每日运势桌面小部件Provider
* 作用: 在Android桌面展示每日运势和幸运关键词,支持深色主题
- * 上次更新内容: 初始创建,支持light/dark双主题布局
+ * 上次更新内容: 改为继承PrivacyAwareHomeWidgetProvider,未同意隐私政策时不执行更新
*/
package apps.xy.xianyan.widget
@@ -15,10 +15,9 @@ import android.widget.RemoteViews
import apps.xy.xianyan.R
import apps.xy.xianyan.MainActivity
import es.antonborri.home_widget.HomeWidgetLaunchIntent
-import es.antonborri.home_widget.HomeWidgetProvider
-class FortuneProvider : HomeWidgetProvider() {
- override fun onUpdate(
+class FortuneProvider : PrivacyAwareHomeWidgetProvider() {
+ override fun onUpdateWithAgreement(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/widget/PomodoroProvider.kt b/android/app/src/main/kotlin/apps/xy/xianyan/widget/PomodoroProvider.kt
index d1ab153d..a1ea8d98 100644
--- a/android/app/src/main/kotlin/apps/xy/xianyan/widget/PomodoroProvider.kt
+++ b/android/app/src/main/kotlin/apps/xy/xianyan/widget/PomodoroProvider.kt
@@ -1,10 +1,10 @@
/**
* PomodoroProvider.kt
* 创建时间: 2026-05-19
- * 更新时间: 2026-05-19
+ * 更新时间: 2026-06-17
* 名称: 番茄钟桌面小部件Provider
* 作用: 在Android桌面展示番茄钟倒计时和状态,支持深色主题
- * 上次更新内容: 初始创建,支持light/dark双主题布局
+ * 上次更新内容: 改为继承PrivacyAwareHomeWidgetProvider,未同意隐私政策时不执行更新
*/
package apps.xy.xianyan.widget
@@ -15,10 +15,9 @@ import android.widget.RemoteViews
import apps.xy.xianyan.R
import apps.xy.xianyan.MainActivity
import es.antonborri.home_widget.HomeWidgetLaunchIntent
-import es.antonborri.home_widget.HomeWidgetProvider
-class PomodoroProvider : HomeWidgetProvider() {
- override fun onUpdate(
+class PomodoroProvider : PrivacyAwareHomeWidgetProvider() {
+ override fun onUpdateWithAgreement(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/widget/PrivacyAwareHomeWidgetProvider.kt b/android/app/src/main/kotlin/apps/xy/xianyan/widget/PrivacyAwareHomeWidgetProvider.kt
new file mode 100644
index 00000000..1e06ba78
--- /dev/null
+++ b/android/app/src/main/kotlin/apps/xy/xianyan/widget/PrivacyAwareHomeWidgetProvider.kt
@@ -0,0 +1,111 @@
+/**
+ * PrivacyAwareHomeWidgetProvider.kt
+ * 创建时间: 2026-06-17
+ * 更新时间: 2026-06-17
+ * 名称: 隐私协议感知的桌面小部件基类
+ * 作用: 所有Widget Provider的基类,在用户未同意隐私政策前阻止Widget更新,
+ * 防止应用退出后被系统广播触发自启动导致隐私合规问题
+ * 上次更新内容: 初始创建,基于HomeWidgetProvider增加隐私协议守门逻辑
+ */
+package apps.xy.xianyan.widget
+
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import android.content.SharedPreferences
+import android.util.Log
+import android.view.View
+import android.widget.RemoteViews
+import apps.xy.xianyan.R
+import es.antonborri.home_widget.HomeWidgetProvider
+
+/**
+ * 隐私协议感知的桌面小部件基类
+ *
+ * 核心设计:
+ * - 在 onUpdate 中检查原生 SharedPreferences 的 agreement_accepted 标志
+ * - 未同意隐私政策时,显示"请先同意隐私政策"占位视图,不读取任何业务数据
+ * - 已同意隐私政策时,调用子类实现的 [onUpdateWithAgreement] 执行正常更新
+ *
+ * 合规依据:
+ * - 《个人信息保护法》要求:处理个人信息前需取得个人同意
+ * - 应用商店审核要求:未同意隐私政策前不得自启动或读取用户数据
+ * - androidx.glance.appwidget 自启动问题修复:即使系统触发 APPWIDGET_UPDATE,
+ * 未同意协议也不执行任何数据操作
+ */
+abstract class PrivacyAwareHomeWidgetProvider : HomeWidgetProvider() {
+
+ companion object {
+ private const val TAG = "PrivacyWidget"
+ // 与 SplashActivity / MainActivity 共用的原生 SharedPreferences
+ private const val PREFS_NAME = "xianyan_prefs"
+ private const val KEY_AGREEMENT_ACCEPTED = "agreement_accepted"
+ }
+
+ /**
+ * 检查用户是否已同意隐私政策
+ * 读取原生 SharedPreferences,与 SplashActivity 使用同一存储
+ */
+ private fun isAgreementAccepted(context: Context): Boolean {
+ val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
+ return prefs.getBoolean(KEY_AGREEMENT_ACCEPTED, false)
+ }
+
+ /**
+ * HomeWidgetProvider 的 onUpdate 入口
+ * 在此进行隐私协议检查,决定是否执行业务更新
+ */
+ final override fun onUpdate(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetIds: IntArray,
+ widgetData: SharedPreferences
+ ) {
+ if (!isAgreementAccepted(context)) {
+ // 未同意隐私政策 — 显示占位视图,不读取任何业务数据
+ Log.w(TAG, "${this::class.simpleName}: 隐私政策未同意,显示占位视图")
+ showPrivacyPlaceholder(context, appWidgetManager, appWidgetIds)
+ return
+ }
+
+ // 已同意隐私政策 — 执行子类的正常更新逻辑
+ onUpdateWithAgreement(context, appWidgetManager, appWidgetIds, widgetData)
+ }
+
+ /**
+ * 显示隐私政策未同意时的占位视图
+ * 提示用户需先打开应用同意隐私政策,点击可跳转到 SplashActivity
+ */
+ private fun showPrivacyPlaceholder(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetIds: IntArray
+ ) {
+ appWidgetIds.forEach { widgetId ->
+ val views = RemoteViews(context.packageName, R.layout.widget_privacy_placeholder).apply {
+ // 点击跳转到应用(通过 SplashActivity 的 LAUNCHER intent)
+ val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
+ if (launchIntent != null) {
+ val pendingIntent = android.app.PendingIntent.getActivity(
+ context,
+ widgetId,
+ launchIntent,
+ android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_IMMUTABLE
+ )
+ setOnClickPendingIntent(R.id.widget_privacy_container, pendingIntent)
+ }
+ }
+ appWidgetManager.updateAppWidget(widgetId, views)
+ }
+ }
+
+ /**
+ * 子类实现的更新逻辑,仅在用户已同意隐私政策后调用
+ * 与 HomeWidgetProvider.onUpdate 签名一致
+ */
+ abstract fun onUpdateWithAgreement(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetIds: IntArray,
+ widgetData: SharedPreferences
+ )
+}
diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/widget/ReadlaterProvider.kt b/android/app/src/main/kotlin/apps/xy/xianyan/widget/ReadlaterProvider.kt
index b679fdf7..c2accbe1 100644
--- a/android/app/src/main/kotlin/apps/xy/xianyan/widget/ReadlaterProvider.kt
+++ b/android/app/src/main/kotlin/apps/xy/xianyan/widget/ReadlaterProvider.kt
@@ -1,10 +1,10 @@
/**
* ReadlaterProvider.kt
* 创建时间: 2026-05-19
- * 更新时间: 2026-05-19
+ * 更新时间: 2026-06-17
* 名称: 稍后读桌面小部件Provider
* 作用: 在Android桌面展示稍后读未读数量和预览,支持深色主题
- * 上次更新内容: 新增dark主题支持,根据widget_theme_mode切换布局
+ * 上次更新内容: 改为继承PrivacyAwareHomeWidgetProvider,未同意隐私政策时不执行更新
*/
package apps.xy.xianyan.widget
@@ -15,10 +15,9 @@ import android.widget.RemoteViews
import apps.xy.xianyan.R
import apps.xy.xianyan.MainActivity
import es.antonborri.home_widget.HomeWidgetLaunchIntent
-import es.antonborri.home_widget.HomeWidgetProvider
-class ReadlaterProvider : HomeWidgetProvider() {
- override fun onUpdate(
+class ReadlaterProvider : PrivacyAwareHomeWidgetProvider() {
+ override fun onUpdateWithAgreement(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
diff --git a/android/app/src/main/kotlin/apps/xy/xianyan/widget/SolarTermProvider.kt b/android/app/src/main/kotlin/apps/xy/xianyan/widget/SolarTermProvider.kt
index 7ac6b44d..87e74756 100644
--- a/android/app/src/main/kotlin/apps/xy/xianyan/widget/SolarTermProvider.kt
+++ b/android/app/src/main/kotlin/apps/xy/xianyan/widget/SolarTermProvider.kt
@@ -1,10 +1,10 @@
/**
* SolarTermProvider.kt
* 创建时间: 2026-05-19
- * 更新时间: 2026-05-19
+ * 更新时间: 2026-06-17
* 名称: 节气诗词桌面小部件Provider
* 作用: 在Android桌面展示当前节气与对应诗词,支持深色主题
- * 上次更新内容: 初始创建,支持light/dark双主题布局
+ * 上次更新内容: 改为继承PrivacyAwareHomeWidgetProvider,未同意隐私政策时不执行更新
*/
package apps.xy.xianyan.widget
@@ -15,10 +15,9 @@ import android.widget.RemoteViews
import apps.xy.xianyan.R
import apps.xy.xianyan.MainActivity
import es.antonborri.home_widget.HomeWidgetLaunchIntent
-import es.antonborri.home_widget.HomeWidgetProvider
-class SolarTermProvider : HomeWidgetProvider() {
- override fun onUpdate(
+class SolarTermProvider : PrivacyAwareHomeWidgetProvider() {
+ override fun onUpdateWithAgreement(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
diff --git a/android/app/src/main/res/layout/widget_privacy_placeholder.xml b/android/app/src/main/res/layout/widget_privacy_placeholder.xml
new file mode 100644
index 00000000..31008480
--- /dev/null
+++ b/android/app/src/main/res/layout/widget_privacy_placeholder.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_shortcut.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_shortcut.png
new file mode 100644
index 00000000..2d89b837
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_shortcut.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_shortcut.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_shortcut.png
new file mode 100644
index 00000000..e1cf72ef
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_shortcut.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_shortcut.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_shortcut.png
new file mode 100644
index 00000000..bc58a58e
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_shortcut.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_shortcut.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_shortcut.png
new file mode 100644
index 00000000..e8bf90f3
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_shortcut.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_shortcut.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_shortcut.png
new file mode 100644
index 00000000..422c68f1
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_shortcut.png differ
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 1fe0d211..7102929b 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -8,6 +8,7 @@
桌面快捷专注计时
当前节气与对应诗词
连续签到天数和快捷签到
+ 最新笔记内容预览
闲言数据管理
主题个性化
打开主题个性化设置
diff --git a/android/app/src/main/res/xml/shortcuts.xml b/android/app/src/main/res/xml/shortcuts.xml
index ac269f21..70ee60da 100644
--- a/android/app/src/main/res/xml/shortcuts.xml
+++ b/android/app/src/main/res/xml/shortcuts.xml
@@ -2,11 +2,13 @@
+
-
-
+ android:targetClass="apps.xy.xianyan.MainActivity"
+ android:data="xianyan://shortcut/action_theme" />
+
+
-
-
+ android:targetClass="apps.xy.xianyan.MainActivity"
+ android:data="xianyan://shortcut/action_search" />
diff --git a/installer.iss b/installer.iss
new file mode 100644
index 00000000..259b35a0
--- /dev/null
+++ b/installer.iss
@@ -0,0 +1,77 @@
+; ============================================================================
+; 闲言APP - Inno Setup 安装脚本
+; ============================================================================
+; 使用方法:
+; 方式一(推荐,自动读取版本号):
+; .\scripts\package_windows.ps1
+; 方式二(手动指定版本号):
+; & "d:\Program Files (x86)\Inno Setup 6\ISCC.exe" /DAppVer=6.78.0 installer.iss
+; 方式三(flutter_distributor):
+; $env:PATH = "E:\cache\pub\bin;" + $env:PATH
+; flutter_distributor package --platform windows --target inno
+;
+; 前置条件:
+; 1. 已安装 Inno Setup 6: https://jrsoftware.org/isdl.php
+; 2. 已下载中文语言包到 Inno Setup Languages 目录
+; 3. 已构建 Flutter 应用: flutter build windows
+; ============================================================================
+
+; ---- 动态版本号(通过 ISCC 命令行 /DAppVer=x.x.x 传入) ----
+; 由 package_windows.ps1 自动传递,无需手动设置
+#ifndef AppVer
+ #define AppVer "0.0.0"
+#endif
+
+[Setup]
+; ---- 应用信息 ----
+AppName=闲言
+AppVersion={#AppVer}
+AppPublisher=微风暴工作室
+AppPublisherURL=https://www.wktyl.com
+AppSupportURL=https://www.wktyl.com
+
+; ---- 安装路径 ----
+DefaultDirName={autopf}\xianyan
+DefaultGroupName=闲言
+UninstallDisplayName=闲言
+
+; ---- 输出配置 ----
+OutputDir=dist
+OutputBaseFilename=闲言_Setup_{#AppVer}
+
+; ---- 压缩配置 ----
+Compression=lzma2/max
+SolidCompression=yes
+
+; ---- 图标和权限 ----
+SetupIconFile=windows\runner\resources\app_icon.ico
+PrivilegesRequired=lowest
+ArchitecturesAllowed=x64compatible
+ArchitecturesInstallIn64BitMode=x64compatible
+
+[Languages]
+Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl"
+
+[Tasks]
+Name: "desktopicon"; Description: "创建桌面快捷方式"; GroupDescription: "附加图标:"; Flags: unchecked
+
+[Files]
+Source: "build\windows\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
+
+[Icons]
+Name: "{group}\闲言"; Filename: "{app}\xianyan.exe"
+Name: "{autodesktop}\闲言"; Filename: "{app}\xianyan.exe"; Tasks: desktopicon
+
+[Run]
+Filename: "{app}\xianyan.exe"; Description: "启动闲言"; Flags: nowait postinstall skipifsilent
+
+[Code]
+procedure CurStepChanged(CurStep: TSetupStep);
+var
+ ResultCode: Integer;
+begin
+ if CurStep = ssPostInstall then
+ begin
+ Exec('ie4uinit.exe', '-show', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
+ end;
+end;
diff --git a/lib/app/app.dart b/lib/app/app.dart
index dbe56e94..0366079c 100644
--- a/lib/app/app.dart
+++ b/lib/app/app.dart
@@ -1,9 +1,9 @@
/// ============================================================
/// 闲言APP — 应用根组件
/// 创建时间: 2026-04-20
-/// 更新时间: 2026-06-06
+/// 更新时间: 2026-06-16
/// 作用: MaterialApp.router + Riverpod 主题管理 + GlassTheme + flutter_animate + AppLockOverlay
-/// 上次更新: 修复安卓端冷启动快捷方式闪退,添加addPostFrameCallback和异常保护
+/// 上次更新: 标准端路径根据liquidGlass平台能力决定是否使用GlassTheme包裹,桌面端跳过避免Impeller警告
/// ============================================================
import 'dart:async';
@@ -25,6 +25,7 @@ import 'package:flutter/services.dart';
import '../core/services/device/quick_actions_service.dart';
import '../core/services/device/macos_platform_service.dart';
+import '../core/services/device/windows_platform_service.dart';
import '../core/services/data/home_widget_service.dart';
import '../core/storage/database/app_database.dart';
import '../core/services/ui/status_bar_service.dart';
@@ -37,6 +38,7 @@ import '../core/services/device/app_lock_service.dart';
import '../core/services/performance/app_lifecycle_gate.dart';
import '../core/theme/app_theme.dart';
import '../core/utils/logger.dart';
+import '../core/utils/platform/platform_capability.dart';
import '../core/utils/platform/platform_utils.dart' as pu;
import '../core/network/api_client.dart';
import '../core/providers/connectivity_provider.dart';
@@ -330,6 +332,7 @@ class _XianyanAppState extends ConsumerState
WidgetsBinding.instance.platformDispatcher.platformBrightness ==
Brightness.dark;
MacosPlatformService.syncTheme(isDark);
+ WindowsPlatformService.syncTheme(isDark);
}
}
@@ -438,6 +441,7 @@ class _XianyanAppState extends ConsumerState
Brightness.dark,
};
MacosPlatformService.syncTheme(effectiveIsDark);
+ WindowsPlatformService.syncTheme(effectiveIsDark);
return Directionality(
textDirection: textDirection,
@@ -542,30 +546,35 @@ class _XianyanAppState extends ConsumerState
},
);
- return GlassTheme(
- data: GlassThemeData(
- light: GlassThemeVariant(
- settings: GlassThemeSettings(
- thickness: 20.0,
- blur: settings.glassEnabled ? 2.0 : 0.0,
- refractiveIndex: 1.4,
- lightIntensity: 0.8,
- ambientStrength: 0.4,
- saturation: 1.0,
- ),
- ),
- dark: GlassThemeVariant(
- settings: GlassThemeSettings(
- thickness: 28.0,
- blur: settings.glassEnabled ? 3.0 : 0.0,
- lightIntensity: 1.0,
- refractiveIndex: 1.2,
- saturation: 1.0,
- ),
- ),
- ),
- child: materialApp,
+ final useGlass = PlatformCapabilities.supports(
+ CapabilityKey.liquidGlass,
);
+ return useGlass
+ ? GlassTheme(
+ data: GlassThemeData(
+ light: GlassThemeVariant(
+ settings: GlassThemeSettings(
+ thickness: 20.0,
+ blur: settings.glassEnabled ? 2.0 : 0.0,
+ refractiveIndex: 1.4,
+ lightIntensity: 0.8,
+ ambientStrength: 0.4,
+ saturation: 1.0,
+ ),
+ ),
+ dark: GlassThemeVariant(
+ settings: GlassThemeSettings(
+ thickness: 28.0,
+ blur: settings.glassEnabled ? 3.0 : 0.0,
+ lightIntensity: 1.0,
+ refractiveIndex: 1.2,
+ saturation: 1.0,
+ ),
+ ),
+ ),
+ child: materialApp,
+ )
+ : materialApp;
}
final iconMode = generalSettings.iconMode;
@@ -575,46 +584,47 @@ class _XianyanAppState extends ConsumerState
return AppIconModeScope(
mode: iconMode,
child: KeyboardBackHandler(
- child: Stack(
- children: [
- buildApp(),
- if (isLocked) const Positioned.fill(child: AppLockOverlay()),
- if (!connectivity.isConnected)
- Positioned(
- top: 0,
- left: 0,
- right: 0,
- child: SafeArea(
- child: Container(
- padding: const EdgeInsets.symmetric(
- horizontal: 12,
- vertical: 8,
- ),
- color: CupertinoColors.systemGrey6.withValues(
- alpha: 0.95,
- ),
- child: Row(
- children: [
- const Icon(
- CupertinoIcons.wifi_exclamationmark,
- size: 16,
- color: CupertinoColors.systemGrey,
- ),
- const SizedBox(width: 8),
- Text(
- '网络已断开,部分功能可能不可用',
- style: AppTypography.footnote.copyWith(
+ child: Stack(
+ children: [
+ buildApp(),
+ if (isLocked)
+ const Positioned.fill(child: AppLockOverlay()),
+ if (!connectivity.isConnected)
+ Positioned(
+ top: 0,
+ left: 0,
+ right: 0,
+ child: SafeArea(
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 8,
+ ),
+ color: CupertinoColors.systemGrey6.withValues(
+ alpha: 0.95,
+ ),
+ child: Row(
+ children: [
+ const Icon(
+ CupertinoIcons.wifi_exclamationmark,
+ size: 16,
color: CupertinoColors.systemGrey,
),
- ),
- ],
+ const SizedBox(width: 8),
+ Text(
+ '网络已断开,部分功能可能不可用',
+ style: AppTypography.footnote.copyWith(
+ color: CupertinoColors.systemGrey,
+ ),
+ ),
+ ],
+ ),
),
),
),
- ),
- ],
+ ],
+ ),
),
- ),
);
},
),
diff --git a/lib/core/services/clipboard_monitor_service.dart b/lib/core/services/clipboard_monitor_service.dart
index 5f360eaf..e9c94a6b 100644
--- a/lib/core/services/clipboard_monitor_service.dart
+++ b/lib/core/services/clipboard_monitor_service.dart
@@ -1,16 +1,16 @@
/// ============================================================
/// 闲言APP — 剪贴板链接监控服务
/// 创建时间: 2026-05-15
-/// 更新时间: 2026-06-10
+/// 更新时间: 2026-06-16
/// 作用: 被动检测剪贴板中的URL链接并提示保存到稍后读
-/// 上次更新: 移除定时轮询,改为被动触发(隐私合规),仅用户主动操作时检查
+/// 上次更新: 使用ClipboardBridge统一入口,含隐私协议守卫,
+/// 未同意协议时剪贴板读取返回null,不再触发保存
/// ============================================================
import 'package:xianyan/core/utils/data/pattern_utils.dart';
import 'dart:async';
-import 'package:flutter/services.dart';
-
+import '../../core/utils/platform/clipboard_bridge.dart';
import '../../core/storage/kv_storage.dart';
import '../../core/utils/logger.dart';
import '../../shared/widgets/feedback/app_toast.dart';
@@ -62,8 +62,7 @@ class ClipboardMonitorService {
_lastCheckTime = now;
try {
- final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
- final text = clipboardData?.text;
+ final text = await ClipboardBridge.getData();
if (text == null || text.isEmpty || text == _lastClipboardText) return;
diff --git a/lib/core/services/data/home_widget_service.dart b/lib/core/services/data/home_widget_service.dart
index 673101a8..9048e897 100644
--- a/lib/core/services/data/home_widget_service.dart
+++ b/lib/core/services/data/home_widget_service.dart
@@ -1,9 +1,9 @@
/// ============================================================
/// 闲言APP — 桌面小组件数据管理服务
/// 创建时间: 2026-05-15
-/// 更新时间: 2026-06-05
+/// 更新时间: 2026-06-16
/// 作用: 基于home_widget库管理桌面小组件数据推送与交互
-/// 上次更新: 新增PlatformCapabilities能力查询补充homeWidget判断
+/// 上次更新: 添加平台检测,Windows/Linux桌面端跳过HomeWidget调用防止MissingPluginException
/// ============================================================
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -69,12 +69,20 @@ class HomeWidgetService {
bool get isInitialized => _initialized;
+ /// 当前平台是否支持桌面小组件(仅 iOS/Android/macOS 支持,Windows/Linux 跳过)
+ bool get _isPlatformSupported => !pu.isDesktop || pu.isMacOS;
+
// ============================================================
// 初始化
// ============================================================
Future init() async {
if (_initialized) return;
+ if (!_isPlatformSupported) {
+ _initialized = true;
+ Log.i('HomeWidgetService: 当前平台不支持桌面小组件,跳过初始化');
+ return;
+ }
try {
await HomeWidget.setAppGroupId(_appGroupId);
@@ -92,6 +100,7 @@ class HomeWidgetService {
// ============================================================
Future pushInitialData() async {
+ if (!_isPlatformSupported) return;
try {
await _ensureInit();
@@ -163,6 +172,7 @@ class HomeWidgetService {
// ============================================================
Future updateReadlaterCount(int count) async {
+ if (!_isPlatformSupported) return;
try {
await _ensureInit();
await HomeWidget.saveWidgetData(_keyReadlaterCount, count);
@@ -178,6 +188,7 @@ class HomeWidgetService {
// ============================================================
Future updateReadlaterPreview(String text, String? author) async {
+ if (!_isPlatformSupported) return;
try {
await _ensureInit();
final safeText = text.isEmpty
@@ -203,6 +214,7 @@ class HomeWidgetService {
// ============================================================
Future updateDailySentence(String text, String author) async {
+ if (!_isPlatformSupported) return;
try {
await _ensureInit();
final safeText = text.isEmpty ? '生活不止眼前的苟且' : text;
@@ -224,6 +236,7 @@ class HomeWidgetService {
// ============================================================
Future handleWidgetClick() async {
+ if (!_isPlatformSupported) return;
try {
await _ensureInit();
final data = await HomeWidget.getWidgetData('clicked_data');
@@ -255,7 +268,10 @@ class HomeWidgetService {
final uri = Uri.tryParse(data);
final widgetType = uri?.queryParameters['widget'];
Log.i('HomeWidgetService: Widget刷新请求 — $widgetType');
- KvStorage.setString(_keyPendingNavRoute, '/home?widget_refresh=$widgetType');
+ KvStorage.setString(
+ _keyPendingNavRoute,
+ '/home?widget_refresh=$widgetType',
+ );
return;
}
// 处理 Widget 交互操作(iOS 17+ AppIntent)
@@ -263,7 +279,9 @@ class HomeWidgetService {
final uri = Uri.tryParse(data);
final actionType = uri?.queryParameters['type'];
final contentType = uri?.queryParameters['contentType'];
- Log.i('HomeWidgetService: Widget交互操作 — type=$actionType, contentType=$contentType');
+ Log.i(
+ 'HomeWidgetService: Widget交互操作 — type=$actionType, contentType=$contentType',
+ );
_handleInteractiveAction(actionType ?? '', contentType);
return;
}
@@ -299,16 +317,27 @@ class HomeWidgetService {
break;
case 'next':
// 切换下一条内容 - 触发刷新并切换
- final widgetType = Uri.tryParse(KvStorage.getString('clicked_data') ?? '')?.queryParameters['widget'];
- KvStorage.setString(_keyPendingNavRoute, '/home?widget_action=next&widget=$widgetType');
+ final widgetType = Uri.tryParse(
+ KvStorage.getString('clicked_data') ?? '',
+ )?.queryParameters['widget'];
+ KvStorage.setString(
+ _keyPendingNavRoute,
+ '/home?widget_action=next&widget=$widgetType',
+ );
break;
case 'checkin':
// 执行签到
- KvStorage.setString(_keyPendingNavRoute, '/signin?widget_action=checkin');
+ KvStorage.setString(
+ _keyPendingNavRoute,
+ '/signin?widget_action=checkin',
+ );
break;
case 'save_card':
// 保存日签卡片
- KvStorage.setString(_keyPendingNavRoute, '/home?widget_action=save_card');
+ KvStorage.setString(
+ _keyPendingNavRoute,
+ '/home?widget_action=save_card',
+ );
break;
default:
Log.w('HomeWidgetService: 未知的交互操作类型 — $actionType');
@@ -330,6 +359,7 @@ class HomeWidgetService {
// ============================================================
void registerInteractivityCallback() {
+ if (!_isPlatformSupported) return;
try {
HomeWidget.registerInteractivityCallback(_backgroundCallback);
handleWidgetClick();
@@ -352,7 +382,10 @@ class HomeWidgetService {
if (route == '_widget_refresh_action') {
final widgetType = uri.queryParameters['widget'];
Log.i('HomeWidgetService: 后台Widget刷新请求 — $widgetType');
- KvStorage.setString(_keyPendingNavRoute, '/home?widget_refresh=$widgetType');
+ KvStorage.setString(
+ _keyPendingNavRoute,
+ '/home?widget_refresh=$widgetType',
+ );
} else if (route == '_widget_interactive_action') {
// 处理 iOS 17+ AppIntent 交互操作
final actionType = uri.queryParameters['type'];
@@ -374,6 +407,7 @@ class HomeWidgetService {
}
Future pushThemeMode(bool isDark) async {
+ if (!_isPlatformSupported) return;
try {
await _ensureInit();
await HomeWidget.saveWidgetData(
@@ -387,6 +421,7 @@ class HomeWidgetService {
}
Future updateFortune(String text, String keyword) async {
+ if (!_isPlatformSupported) return;
try {
await _ensureInit();
final safeText = text.isEmpty ? '今日运势不错' : text;
@@ -401,6 +436,7 @@ class HomeWidgetService {
}
Future updateCountdown(String title, DateTime target) async {
+ if (!_isPlatformSupported) return;
try {
await _ensureInit();
await HomeWidget.saveWidgetData(_keyCountdownTitle, title);
@@ -416,6 +452,7 @@ class HomeWidgetService {
}
Future updatePomodoro(int remainingSeconds, String state) async {
+ if (!_isPlatformSupported) return;
try {
await _ensureInit();
await HomeWidget.saveWidgetData(
@@ -431,6 +468,7 @@ class HomeWidgetService {
}
Future updateSolarTerm(String name, String poem) async {
+ if (!_isPlatformSupported) return;
try {
await _ensureInit();
final safeName = name.isEmpty ? '立春' : name;
@@ -445,6 +483,7 @@ class HomeWidgetService {
}
Future updateCheckin(int days, bool todayDone) async {
+ if (!_isPlatformSupported) return;
try {
await _ensureInit();
await HomeWidget.saveWidgetData(_keyCheckinDays, days);
@@ -462,6 +501,7 @@ class HomeWidgetService {
String? sentenceId,
String mood = 'happy',
}) async {
+ if (!_isPlatformSupported) return;
try {
await _ensureInit();
final safeContent = content.isEmpty
@@ -498,6 +538,7 @@ class HomeWidgetService {
// ============================================================
Future updateWidget(WidgetType type) async {
+ if (!_isPlatformSupported) return;
try {
await _ensureInit();
@@ -560,6 +601,7 @@ class HomeWidgetService {
// ============================================================
Future