mirror of
https://github.com/gkd-kit/gkd.git
synced 2026-06-03 21:01:49 +08:00
feat: SnapShot Toast in Multiple Situation (#1363)
Some checks failed
Build-Apk / build (push) Has been cancelled
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:
parent
dabf129292
commit
2530f82a06
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user