release: 发布6.6.18版本,完成多项合规与功能优化

此版本包含以下核心更新:
1. 版本号升级至6.6.18,更新全平台配置文件
2. 实现隐私合规改造:
   - 新增剪贴板隐私守卫,未同意协议前禁止读取剪贴板
   - 所有桌面小部件继承隐私感知基类,未同意协议时显示占位提示
   - 移除自动剪贴板监控,改为用户主动触发
3. 新增Windows平台深色主题同步功能
4. 补全多语言默认句子翻译
5. 优化安卓快捷方式配置与小部件合规性
6. 修复macOS插件注册问题
7. 新增Windows安装脚本
8. 优化触觉反馈服务初始化时机
This commit is contained in:
Developer
2026-06-17 04:47:06 +08:00
parent ad00967c68
commit 49b6323772
65 changed files with 1745 additions and 442 deletions

View File

@@ -255,6 +255,15 @@
android:name="androidx.work.impl.background.systemalarm.RescheduleReceiver"
tools:node="remove" />
<!-- 移除 androidx.glance.appwidget 自启动组件,防止应用退出后自启动 -->
<!-- 合规要求未同意隐私政策前不得自启动Glance库未使用但会被合并入Manifest -->
<receiver
android:name="androidx.glance.appwidget.GlanceAppWidgetReceiver"
tools:node="remove" />
<service
android:name="androidx.glance.appwidget.GlanceAppWidgetService"
tools:node="remove" />
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data

View File

@@ -1,15 +1,18 @@
// ============================================================
// 闲言APP — Android主Activity
// 创建时间: 2026-04-20
// 更新时间: 2026-06-13
// 更新时间: 2026-06-16
// 作用: Flutter主入口处理BLE广播 + 管理空间跳转 + 协议安全网 + 快捷方式
// 上次更新: 添加shortcut MethodChannel处理桌面快捷方式跳转
// 保留插件移除/恢复逻辑作为安全网防Shortcut等绕过SplashActivity
// 上次更新: 添加ShortcutManager快捷方式创建增强快捷方式调试日志
// ============================================================
package apps.xy.xianyan
import android.content.Intent
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.graphics.drawable.Icon
import android.os.Build
import android.os.Bundle
import android.util.Log
import apps.xy.xianyan.ble.BleAdvertiserPlugin
@@ -27,6 +30,7 @@ class MainActivity : FlutterActivity() {
private const val BACKGROUND_CHANNEL = "apps.xy.xianyan/background"
private const val AGREEMENT_CHANNEL = "apps.xy.xianyan/agreement"
private const val SHORTCUT_CHANNEL = "apps.xy.xianyan/shortcut"
private const val SHORTCUT_MANAGER_CHANNEL = "apps.xy.xianyan/shortcut_manager"
private const val ACTION_MANAGE_STORAGE = "android.app.action.MANAGE_STORAGE"
// Intent extra key — 桌面快捷方式传递的action类型
@@ -41,18 +45,45 @@ class MainActivity : FlutterActivity() {
// 异常流程Shortcut/DeepLink可能直接启动此Activity此时仍需移除敏感插件
// 注意此安全网无法阻止onAttachedToActivity中的传感器读取注册时已触发
// 完整防护依赖SplashActivity在引擎启动前拦截
//
// 分类说明:
// 🔴 传感器类 — 读取设备传感器数据(加速度/陀螺仪/方向等)
// 🔴 音频类 — 麦克风录音、音频播放
// 🔴 位置类 — GPS/网络定位
// 🔴 网络类 — WiFi信息、网络状态检测
// 🔴 通知类 — 本地推送通知
// 🔴 媒体类 — 相机/相册/视频播放
// 🔴 生物识别类 — 指纹/面容
// 🔴 震动类 — 触觉反馈
private val SENSITIVE_PLUGIN_CLASSES = setOf(
"dev.fluttercommunity.plus.sensors.SensorsPlugin", // 传感器OrientationEventListener读取传感器列表
"com.llfbandit.record.RecordPlugin", // 麦克风录音
"com.csdcorp.speech_to_text.SpeechToTextPlugin", // 语音识别
"com.lyokone.location.LocationPlugin", // 位置
"com.alternadom.wifiiot.WifiIotPlugin", // WiFi
"dev.fluttercommunity.plus.connectivity.ConnectivityPlugin", // 网络状态
"com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsPlugin", // 通知
"io.flutter.plugins.imagepicker.ImagePickerPlugin", // 相机/相册
"io.flutter.plugins.localauth.LocalAuthPlugin", // 生物识别
"com.fluttercandia.photo_manager.PhotoManagerPlugin", // 相册
"io.flutter.plugins.videoplayer.VideoPlayerPlugin", // 视频
// 🔴 传感器
"dev.fluttercommunity.plus.sensors.SensorsPlugin",
// 🔴 麦克风录音
"com.llfbandit.record.RecordPlugin",
// 🔴 语音识别
"com.csdcorp.speech_to_text.SpeechToTextPlugin",
// 🔴 位置
"com.lyokone.location.LocationPlugin",
// 🔴 WiFi信息
"com.alternadom.wifiiot.WifiIotPlugin",
// 🔴 网络状态
"dev.fluttercommunity.plus.connectivity.ConnectivityPlugin",
// 🔴 WiFi网络信息
"dev.fluttercommunity.plus.network_info.NetworkInfoPlugin",
// 🔴 通知
"com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsPlugin",
// 🔴 相机/相册
"io.flutter.plugins.imagepicker.ImagePickerPlugin",
// 🔴 生物识别
"io.flutter.plugins.localauth.LocalAuthPlugin",
// 🔴 相册管理
"com.fluttercandia.photo_manager.PhotoManagerPlugin",
// 🔴 视频播放
"io.flutter.plugins.videoplayer.VideoPlayerPlugin",
// 🔴 音频播放
"xyz.luan.audioplayers.AudioplayersPlugin",
// 🔴 震动/触觉反馈
"com.whelksoft.flutter_vibrate.FlutterVibratePlugin",
)
}
@@ -73,6 +104,7 @@ class MainActivity : FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.i(TAG, "MainActivity.onCreate")
bleAdvertiser.setupChannel(flutterEngine!!, this)
handleManageSpaceIntent(intent)
handleShortcutIntent(intent)
@@ -80,6 +112,7 @@ class MainActivity : FlutterActivity() {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
Log.i(TAG, "MainActivity.onNewIntent: $intent")
setIntent(intent)
handleManageSpaceIntent(intent)
handleShortcutIntent(intent)
@@ -87,12 +120,14 @@ class MainActivity : FlutterActivity() {
override fun onResume() {
super.onResume()
Log.i(TAG, "MainActivity.onResume: pendingShortcutAction=$pendingShortcutAction")
pendingManageSpaceAction?.let { action ->
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

View File

@@ -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
// 确保协议守门不被绕过
//
// 关键MainActivityFlutter引擎只有在用户同意后才被启动
// 因此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)
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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
)
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- widget_privacy_placeholder.xml -->
<!-- 隐私政策未同意时的Widget占位布局 -->
<!-- 合规要求未同意隐私政策前Widget不得展示用户数据 -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget_privacy_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#F5F5F5"
android:padding="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🔒"
android:textSize="24sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="请先同意隐私政策"
android:textColor="#666666"
android:textSize="13sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="点击打开应用"
android:textColor="#999999"
android:textSize="11sp" />
</LinearLayout>
</FrameLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -8,6 +8,7 @@
<string name="widget_pomodoro_desc">桌面快捷专注计时</string>
<string name="widget_solar_term_desc">当前节气与对应诗词</string>
<string name="widget_checkin_desc">连续签到天数和快捷签到</string>
<string name="widget_ctc_latest_note_desc">最新笔记内容预览</string>
<string name="data_management_label">闲言数据管理</string>
<string name="shortcut_theme_short">主题个性化</string>
<string name="shortcut_theme_long">打开主题个性化设置</string>

View File

@@ -2,11 +2,13 @@
<!-- ============================================================
闲言APP — 桌面快捷方式配置
创建时间: 2026-06-09
更新时间: 2026-06-13
作用: 定义长按桌面图标显示的快捷方式
上次更新: 使用矢量drawable替代mipmap图标规范extra key名称
更新时间: 2026-06-16
作用: 定义长按桌面图标显示的快捷方式API 25+ 静态定义)
注意: 此文件定义静态快捷方式,由系统在应用安装时读取显示
实际快捷方式处理由 MainActivity 通过 ShortcutManager API 管理
============================================================ -->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 主题个性化快捷方式 -->
<shortcut
android:shortcutId="action_theme"
android:enabled="true"
@@ -16,10 +18,11 @@
<intent
android:action="android.intent.action.RUN"
android:targetPackage="apps.xy.xianyan"
android:targetClass="apps.xy.xianyan.MainActivity">
<extra android:name="shortcut_action" android:value="action_theme" />
</intent>
android:targetClass="apps.xy.xianyan.MainActivity"
android:data="xianyan://shortcut/action_theme" />
</shortcut>
<!-- 搜索功能快捷方式 -->
<shortcut
android:shortcutId="action_search"
android:enabled="true"
@@ -29,8 +32,7 @@
<intent
android:action="android.intent.action.RUN"
android:targetPackage="apps.xy.xianyan"
android:targetClass="apps.xy.xianyan.MainActivity">
<extra android:name="shortcut_action" android:value="action_search" />
</intent>
android:targetClass="apps.xy.xianyan.MainActivity"
android:data="xianyan://shortcut/action_search" />
</shortcut>
</shortcuts>