Skip to content

Commit a59884b

Browse files
committed
improvement(Windows): Support window capture protection #1424
1 parent c92c7dd commit a59884b

4 files changed

Lines changed: 108 additions & 0 deletions

File tree

common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/settings/component/SettingScreenshots.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ fun settingScreenshotsProvider(
7878
SettingIi(
7979
platformClasses = listOf(
8080
Platform.Mobile::class,
81+
Platform.Desktop.Windows::class,
8182
),
8283
search = SettingIi.Search(
8384
group = "conceal",
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.artemchep.keyguard.ui
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.LaunchedEffect
5+
import arrow.core.throwIfFatal
6+
import com.artemchep.jna.windows.setWindowExcludedFromCapture
7+
import com.artemchep.keyguard.common.model.AllowScreenshots
8+
import com.artemchep.keyguard.common.usecase.GetAllowScreenshots
9+
import com.artemchep.keyguard.platform.CurrentPlatform
10+
import com.artemchep.keyguard.platform.Platform
11+
import com.artemchep.keyguard.platform.recordException
12+
import com.artemchep.keyguard.platform.recordLog
13+
import kotlinx.coroutines.flow.collect
14+
import kotlinx.coroutines.flow.distinctUntilChanged
15+
import kotlinx.coroutines.flow.onEach
16+
import org.kodein.di.compose.rememberInstance
17+
18+
@Composable
19+
fun WindowScreenshotProtectionEffect() {
20+
if (CurrentPlatform !is Platform.Desktop.Windows) {
21+
return
22+
}
23+
24+
val window = LocalComposeWindow.current
25+
val windowHandle = window.windowHandle
26+
val getAllowScreenshots by rememberInstance<GetAllowScreenshots>()
27+
LaunchedEffect(
28+
getAllowScreenshots,
29+
windowHandle,
30+
) {
31+
getAllowScreenshots()
32+
.distinctUntilChanged()
33+
.onEach { allowScreenshots ->
34+
val excluded = allowScreenshots < AllowScreenshots.LIMITED
35+
applyWindowScreenshotProtection(
36+
windowHandle = windowHandle,
37+
excluded = excluded,
38+
)
39+
}
40+
.collect()
41+
}
42+
}
43+
44+
private fun applyWindowScreenshotProtection(
45+
windowHandle: Long,
46+
excluded: Boolean,
47+
) {
48+
try {
49+
val success = setWindowExcludedFromCapture(
50+
windowHandle = windowHandle,
51+
excluded = excluded,
52+
)
53+
if (!success) {
54+
recordLog("Failed to update Windows screenshot protection.")
55+
}
56+
} catch (e: Throwable) {
57+
e.throwIfFatal()
58+
recordException(e)
59+
}
60+
}

desktopApp/src/jvmMain/kotlin/com/artemchep/keyguard/Main.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ import com.artemchep.keyguard.platform.util.isRelease
9696
import com.artemchep.keyguard.res.Res
9797
import com.artemchep.keyguard.res.*
9898
import com.artemchep.keyguard.ui.LocalComposeWindow
99+
import com.artemchep.keyguard.ui.WindowScreenshotProtectionEffect
99100
import com.artemchep.keyguard.ui.surface.LocalBackgroundManager
100101
import com.artemchep.keyguard.ui.surface.LocalSurfaceColor
101102
import com.artemchep.keyguard.ui.theme.GlobalExpressive
@@ -597,6 +598,8 @@ internal fun FrameWindowScope.KeyguardWindowEssentials(
597598
LocalComposeWindow provides this.window,
598599
LocalWindowId provides windowId,
599600
) {
601+
WindowScreenshotProtectionEffect()
602+
600603
LaunchLifecycleProviderEffect(
601604
processLifecycleProvider = processLifecycleProvider,
602605
)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.artemchep.jna.windows
2+
3+
import com.sun.jna.Native
4+
import com.sun.jna.Pointer
5+
import com.sun.jna.win32.StdCallLibrary
6+
7+
private const val WDA_NONE = 0x00000000
8+
private const val WDA_EXCLUDEFROMCAPTURE = 0x00000011
9+
10+
public fun setWindowExcludedFromCapture(
11+
windowHandle: Long,
12+
excluded: Boolean,
13+
): Boolean {
14+
if (windowHandle == 0L) {
15+
return false
16+
}
17+
18+
val affinity = if (excluded) {
19+
WDA_EXCLUDEFROMCAPTURE
20+
} else {
21+
WDA_NONE
22+
}
23+
return User32.INSTANCE.SetWindowDisplayAffinity(
24+
Pointer.createConstant(windowHandle),
25+
affinity,
26+
)
27+
}
28+
29+
@Suppress("FunctionName")
30+
private interface User32 : StdCallLibrary {
31+
companion object {
32+
val INSTANCE: User32 by lazy {
33+
Native.load(
34+
"user32",
35+
User32::class.java,
36+
) as User32
37+
}
38+
}
39+
40+
fun SetWindowDisplayAffinity(
41+
hWnd: Pointer,
42+
dwAffinity: Int,
43+
): Boolean
44+
}

0 commit comments

Comments
 (0)