feat: SnapShot Toast in Multiple Situation (#1363)
Some checks failed
Build-Apk / build (push) Has been cancelled

* feat: 快照成功toast判断截图tip

"快照成功"判断是否存在截图toast提示

* perf: 简写为also函数 & 布尔值语义纠正

* feat: 增加第三种情况demo

- 新增"目标App疑似截屏保护",判定逻辑ing
- when枚举

* feat: App拒绝提供画面逻辑 (方差法)

* fix: gpu位图崩溃隐患&防截屏判定逻辑

- 修复部分设备上 HARDWARE 位图无法直接读取像素导致的崩溃问题(强制转为 ARGB_8888)
- 引入 `recycleIfTemp` 辅助函数,统一并安全回收临时 Bitmap
- 增加裁剪状态栏逻辑,并将边缘忽略比例由 0.2 调整为 0.1
- 优化判定算法:引入纯黑极值像素占比统计,放宽方差阈值

* perf: 删除状态栏预处理&降低图片边缘排除阈值

- 调用函数涂黑属于写操作,且与忽略图片边缘操作本质相同,纯副操作.故删除
- 调低排除四周图片阈值至8%,降低纯黑界面却有字(正常)情况下错误识别

* perf: 死代码

bitmap !== bitmap为不成立,啥也没干

* perf: 简化toast语句

* fix: toast text

---------

Co-authored-by: 二刺螈 <i@songe.li>
This commit is contained in:
Ling0402 2026-05-16 08:33:55 +08:00 committed by GitHub
parent dabf129292
commit 2530f82a06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -2,6 +2,7 @@ package li.songe.gkd.util
import android.graphics.Bitmap
import androidx.core.graphics.createBitmap
import androidx.core.graphics.scale
import androidx.core.graphics.set
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
@ -122,7 +123,73 @@ object SnapshotExt {
}
return tempBp
}
// 截图三种状态
private enum class ScreenWhy {
Pass,
NotHave,
Block,
}
// App拒绝提供画面判定逻辑
private fun isAppProtected(bitmap: Bitmap): Boolean {
fun Bitmap.recycleIfTemp() { if (this !== bitmap) recycle() }
// 缩小图片
val size = 64
val scaled = bitmap.scale(size, size, false)
/* 强制转为 ARGB_8888软件位图
部分设备,Android版本scale()返回仍是HARDWARE
bitmap在 gpu内存,cpu无法直接读,会崩溃
*/
val softBitmap = if (scaled.config == Bitmap.Config.HARDWARE) {
val copy = scaled.copy(Bitmap.Config.ARGB_8888, false)
scaled.recycleIfTemp()
copy ?: return false // copy 失败(极端 OOM直接返回不继续执行
} else {
scaled
}
// 像素一次性读取到数组
val pixels = IntArray(size * size)
softBitmap.getPixels(pixels, 0, size, 0, 0, size, size)
softBitmap.recycleIfTemp()
val ignore = (size * 0.08).toInt() // 忽略图片边缘
// 统计变量
var sum = 0.0 // 亮度总和
var sumSq = 0.0 // 平方总和
var count = 0 // 样本数量
// 统计极值像素占比
var nearBlackCount = 0
// 采样
val step = 2 //隔一个像素取样
for (y in ignore until size - ignore step step) { // ignore(忽略边缘)
for (x in ignore until size - ignore step step) {
// 提取RGB
val p = pixels[y * size + x]
val r = (p shr 16) and 0xff
val g = (p shr 8) and 0xff
val b = p and 0xff
// 计算亮度
val l = 0.299 * r + 0.587 * g + 0.114 * b
// 统计
sum += l
sumSq += l * l
count++
if (l < 10) nearBlackCount++
}
}
// 防止除零
if (count == 0) return false
// 平均值和方差计算
val mean = sum / count
val variance = sumSq / count - mean * mean
// 极值(纯黑)像素占比
val blackRatio = nearBlackCount.toDouble() / count
// 判断条件拆分,低方差+像素高度集中在极端值
val isNearlyUniform = variance < 15.0 // 放宽,包容轻微噪点
val isDominantlyBlack = blackRatio > 0.85 && mean < 15.0
// 判断值设定
return isNearlyUniform && (isDominantlyBlack)
}
private val captureLoading = MutableStateFlow(false)
suspend fun captureSnapshot(forcedCropStatusBar: Boolean = false): ComplexSnapshot {
if (A11yRuleEngine.instance == null) {
@ -139,7 +206,7 @@ object SnapshotExt {
if (storeFlow.value.showSaveSnapshotToast) {
toast("正在保存快照...", forced = true)
}
val (snapshot, bitmap) = coroutineScope {
val (snapshot, screenResult) = coroutineScope { // 快照数据+截图(图片 && 状态)
val d1 = async(Dispatchers.IO) {
val appId = rootNode.packageName.toString()
var activityId = shizukuContextFlow.value.topCpn()?.className
@ -168,19 +235,34 @@ object SnapshotExt {
)
}
val d2 = async(Dispatchers.IO) {
(A11yRuleEngine.screenshot()
?: ScreenshotService.screenshot()
?: emptyScreenBitmap("无截图权限\n请自行替换")
).let {
if (storeFlow.value.hideSnapshotStatusBar && (forcedCropStatusBar || BarUtils.checkStatusBarVisible() == true)) {
cropBitmapStatusBar(it)
} else {
it
}
val rawPicture = // 获取原始图片
A11yRuleEngine.screenshot() // 无障碍
?: ScreenshotService.screenshot() // 截图服务
val (finalBitmap, status) = when {
rawPicture == null -> {
emptyScreenBitmap("无截图权限\n请自行替换") to ScreenWhy.NotHave
}
isAppProtected(rawPicture) -> {
rawPicture to ScreenWhy.Block
}
else -> {
rawPicture to ScreenWhy.Pass
}
}
val processedBitmap = if (status == ScreenWhy.Pass &&
storeFlow.value.hideSnapshotStatusBar && (forcedCropStatusBar || BarUtils.checkStatusBarVisible() == true)) {
cropBitmapStatusBar(finalBitmap)
} else {
finalBitmap
}
processedBitmap to status
}
d1.await() to d2.await()
}
val (bitmap, currentStatus) = screenResult // 拆开(图片+状态)
withContext(Dispatchers.IO) {
snapshotParentPath(snapshot.id).autoMk()
screenshotFile(snapshot.id).outputStream().use { stream ->
@ -196,7 +278,12 @@ object SnapshotExt {
)
DbSet.snapshotDao.insert(snapshot.toSnapshot())
}
toast("快照成功", forced = true)
val tip = when (currentStatus) {
ScreenWhy.NotHave -> "快照成功 (无截图)"
ScreenWhy.Block -> "快照成功 (应用可能禁止截图)"
ScreenWhy.Pass -> "快照成功"
}
toast(tip, forced = true)
val desc = snapshot.appInfo?.name ?: snapshot.appId
snapshotNotif.copy(text = "快照「$desc」已保存至记录").notifySelf()
return snapshot