Skip to content

Commit 91501ce

Browse files
committed
fix(android): deprecate preventScreenshots and make dialog more reliable
1 parent 03ea661 commit 91501ce

File tree

4 files changed

+94
-35
lines changed

4 files changed

+94
-35
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
## [Unreleased]
2+
3+
### Bug Fixes
4+
5+
* **android:** more reliable handling of the privacy screen in different navigation modes.
6+
7+
### Deprecated
8+
9+
* **android:** `preventScreenshots` config option is now deprecated. FLAG_SECURE is automatically applied when privacy screen is enabled, providing screenshot prevention and app switcher protection. This option will be removed in a future major version. To control screenshot prevention per screen, enable/disable the plugin as needed on specific screens.
10+
11+
### Documentation
12+
13+
* **android:** Added comprehensive documentation for FLAG_SECURE behavior, including screenshot prevention, app switcher protection, and non-secure display restrictions
14+
* **android:** Clarified live view protection: when FLAG_SECURE doesn't fully protect content (e.g., gesture navigation or live views that can persist for minutes), a temporary privacy screen overlay is displayed
15+
* Added per-screen enable/disable example
16+
117
## [1.1.1](https://github.com/ionic-team/capacitor-privacy-screen/compare/v1.1.0...v1.1.1) (2025-08-21)
218

319

README.md

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,71 @@ npx cap sync
1414
### Platform Notes
1515

1616
#### Android
17-
The privacy screen behavior on Android varies depending on the navigation method used:
18-
- When using gesture navigation or the recent apps button, the privacy screen will display as configured
19-
- When using the home button to exit the app, the system must fall back to using [`FLAG_SECURE`](https://developer.android.com/reference/android/view/WindowManager.LayoutParams#FLAG_SECURE) as it's the only way to prevent content visibility in this scenario
17+
18+
##### FLAG_SECURE Behavior
19+
When the privacy screen is enabled, the plugin automatically applies Android's [`FLAG_SECURE`](https://developer.android.com/reference/android/view/WindowManager.LayoutParams#FLAG_SECURE) flag to the window. This provides comprehensive content protection:
20+
21+
- **Screenshot Prevention**: Prevents users from taking screenshots or screen recordings of your app
22+
- **App Switcher/Recent Apps**: When the app appears in the recent apps view, FLAG_SECURE causes the system to show either a black screen or the last frame captured before FLAG_SECURE was applied (typically blank)
23+
- **Non-Secure Display Protection**: Prevents the window content from appearing on non-secure displays such as TVs, projectors, or screen mirroring to untrusted devices
24+
- **Live View Protection**: In cases where FLAG_SECURE doesn't fully protect content (such as with gesture navigation or live view fragments that can persist for minutes), the plugin displays a temporary privacy screen overlay. This overlay can be configured via `dimBackground` (shows a dim overlay) or shows the splash screen by default.
25+
26+
##### Navigation Method Differences
27+
The privacy screen behavior varies depending on how the user navigates away from the app:
28+
- **Recent Apps Button/Gesture**: The privacy dialog displays as configured when viewing the app switcher
29+
- **Home Button**: FLAG_SECURE ensures content protection in the app switcher snapshot
30+
- **Activity Background Events**: Controlled separately via `privacyModeOnActivityHidden` for scenarios like biometric prompts
31+
32+
## Usage
33+
34+
### Basic Usage
35+
36+
```typescript
37+
import { PrivacyScreen } from '@capacitor/privacy-screen';
38+
39+
// Enable privacy screen with default settings
40+
await PrivacyScreen.enable();
41+
42+
// Enable with platform-specific configuration
43+
await PrivacyScreen.enable({
44+
android: {
45+
dimBackground: true,
46+
privacyModeOnActivityHidden: 'splash'
47+
},
48+
ios: {
49+
blurEffect: 'dark'
50+
}
51+
});
52+
53+
// Disable privacy screen
54+
await PrivacyScreen.disable();
55+
56+
// Check if privacy screen is enabled
57+
const { enabled } = await PrivacyScreen.isEnabled();
58+
```
59+
60+
### Per-Screen Protection
61+
62+
You can enable and disable the privacy screen on specific screens by calling `enable()` when entering a screen and `disable()` when leaving:
63+
64+
```typescript
65+
import { PrivacyScreen } from '@capacitor/privacy-screen';
66+
67+
// When navigating to a secure screen
68+
async function navigateToSecureScreen() {
69+
await PrivacyScreen.enable({
70+
android: { dimBackground: true },
71+
ios: { blurEffect: 'dark' }
72+
});
73+
// Navigate to your secure screen
74+
}
75+
76+
// When navigating away from a secure screen
77+
async function navigateAwayFromSecureScreen() {
78+
await PrivacyScreen.disable();
79+
// Navigate to your next screen
80+
}
81+
```
2082

2183
## API
2284

android/src/main/java/com/capacitorjs/plugins/privacyscreen/PrivacyScreenPlugin.kt

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,13 @@ enum class PrivacyMode {
2828
@CapacitorPlugin(name = "PrivacyScreen")
2929
class PrivacyScreenPlugin : Plugin() {
3030
private var privacyScreenEnabled = false
31-
private var preventScreenshots = false
3231
private var dimBackground = false
3332
private var privacyModeOnActivityHidden = PrivacyMode.NONE
3433
private var dialog: PrivacyScreenDialog? = null
3534

3635
private val recentAppsReceiver = object : BroadcastReceiver() {
3736
override fun onReceive(context: Context, intent: Intent) {
38-
if ((Build.VERSION.SDK_INT >= 31 || usesGestureNavigation(context)) &&
39-
ACTION_CLOSE_SYSTEM_DIALOGS == intent.action) {
37+
if (ACTION_CLOSE_SYSTEM_DIALOGS == intent.action) {
4038
val reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY)
4139
if (reason != null) {
4240
when (reason) {
@@ -64,19 +62,14 @@ class PrivacyScreenPlugin : Plugin() {
6462
try {
6563
val config = call.getObject("android")
6664
dimBackground = config?.optBoolean("dimBackground") ?: false
67-
preventScreenshots = config?.optBoolean("preventScreenshots") ?: false
6865
privacyModeOnActivityHidden = when (config?.optString("privacyModeOnActivityHidden", "none")) {
6966
"dim" -> PrivacyMode.DIM
7067
"splash" -> PrivacyMode.SPLASH
7168
else -> PrivacyMode.NONE
7269
}
7370

7471
activity.runOnUiThread {
75-
if (preventScreenshots) {
76-
activity.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
77-
} else {
78-
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
79-
}
72+
activity.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
8073
privacyScreenEnabled = true
8174
dialog = PrivacyScreenDialog(activity, dimBackground)
8275

@@ -94,7 +87,6 @@ class PrivacyScreenPlugin : Plugin() {
9487
@PluginMethod
9588
fun disable(call: PluginCall) {
9689
privacyScreenEnabled = false
97-
preventScreenshots = false
9890
privacyModeOnActivityHidden = PrivacyMode.NONE
9991
activity.runOnUiThread {
10092
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
@@ -116,17 +108,11 @@ class PrivacyScreenPlugin : Plugin() {
116108
override fun handleOnResume() {
117109
super.handleOnResume()
118110
onRecentAppsTriggered(false)
119-
if (preventScreenshots) {
120-
activity.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
121-
} else {
122-
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
123-
}
124111
}
125112

126113
override fun handleOnPause() {
127114
super.handleOnPause()
128115
if (privacyScreenEnabled) {
129-
activity.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
130116
when (privacyModeOnActivityHidden) {
131117
PrivacyMode.DIM -> showPrivacyDialog(true)
132118
PrivacyMode.SPLASH -> showPrivacyDialog(false)
@@ -145,15 +131,17 @@ class PrivacyScreenPlugin : Plugin() {
145131
}
146132

147133
private fun showPrivacyDialog(dim: Boolean) {
148-
if (dialog != null && isDialogViewAttachedToWindowManager()) {
134+
if (dialog?.isShowing == true || isDialogViewAttachedToWindowManager()) {
135+
return
136+
}
137+
138+
if (dialog != null) {
149139
dialog?.dismiss()
150140
dialog = null
151141
}
142+
152143
context.let { context ->
153-
if (context is AppCompatActivity &&
154-
!context.isFinishing &&
155-
dialog?.isShowing != true &&
156-
!isDialogViewAttachedToWindowManager()) {
144+
if (context is AppCompatActivity && !context.isFinishing) {
157145
dialog = PrivacyScreenDialog(context, dim)
158146
dialog?.show()
159147
}
@@ -164,13 +152,6 @@ class PrivacyScreenPlugin : Plugin() {
164152
return dialog?.window?.decorView?.parent != null
165153
}
166154

167-
@SuppressLint("DiscouragedApi")
168-
private fun usesGestureNavigation(context: Context): Boolean {
169-
val resources = context.resources
170-
val resourceId = resources.getIdentifier("config_navBarInteractionMode", "integer", "android")
171-
return resourceId > 0 && resources.getInteger(resourceId) == 2
172-
}
173-
174155
override fun handleOnDestroy() {
175156
dialog?.dismiss()
176157
dialog = null

src/definitions.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ export interface PrivacyScreenConfig {
1010
dimBackground?: boolean;
1111

1212
/**
13-
* Controls whether screenshots can be taken while the app is in use.
14-
* This uses `FLAG_SECURE` so it will also prevent the window from being displayed on a non-secure display, such as a TV or projector.`
15-
* Note: Privacy screen protection in app switcher is always enabled when the plugin is enabled.
16-
* @default false
13+
* @deprecated This option is no longer necessary. FLAG_SECURE is now always applied when the privacy screen is enabled,
14+
* which prevents screenshots and protects content in the app switcher. This option will be removed in a future version.
15+
*
16+
* If you need to control screenshot prevention separately, you can enable/disable the plugin as needed per screen.
1717
*/
1818
preventScreenshots?: boolean;
1919

0 commit comments

Comments
 (0)