Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
## [Unreleased]

### Bug Fixes

* **android:** more reliable handling of the privacy screen in different navigation modes.

### Deprecated

* **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.

### Documentation

* **android:** Added comprehensive documentation for FLAG_SECURE behavior, including screenshot prevention, app switcher protection, and non-secure display restrictions
* **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
* Added per-screen enable/disable example

## [1.1.1](https://github.com/ionic-team/capacitor-privacy-screen/compare/v1.1.0...v1.1.1) (2025-08-21)


Expand Down
68 changes: 65 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,71 @@ npx cap sync
### Platform Notes

#### Android
The privacy screen behavior on Android varies depending on the navigation method used:
- When using gesture navigation or the recent apps button, the privacy screen will display as configured
- 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

##### FLAG_SECURE Behavior
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:

- **Screenshot Prevention**: Prevents users from taking screenshots or screen recordings of your app
- **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)
- **Non-Secure Display Protection**: Prevents the window content from appearing on non-secure displays such as TVs, projectors, or screen mirroring to untrusted devices
- **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.

##### Navigation Method Differences
The privacy screen behavior varies depending on how the user navigates away from the app:
- **Recent Apps Button/Gesture**: The privacy dialog displays as configured when viewing the app switcher
- **Home Button**: FLAG_SECURE ensures content protection in the app switcher snapshot
- **Activity Background Events**: Controlled separately via `privacyModeOnActivityHidden` for scenarios like biometric prompts

## Usage

### Basic Usage

```typescript
import { PrivacyScreen } from '@capacitor/privacy-screen';

// Enable privacy screen with default settings
await PrivacyScreen.enable();

// Enable with platform-specific configuration
await PrivacyScreen.enable({
android: {
dimBackground: true,
privacyModeOnActivityHidden: 'splash'
},
ios: {
blurEffect: 'dark'
}
});

// Disable privacy screen
await PrivacyScreen.disable();

// Check if privacy screen is enabled
const { enabled } = await PrivacyScreen.isEnabled();
```

### Per-Screen Protection

You can enable and disable the privacy screen on specific screens by calling `enable()` when entering a screen and `disable()` when leaving:

```typescript
import { PrivacyScreen } from '@capacitor/privacy-screen';

// When navigating to a secure screen
async function navigateToSecureScreen() {
await PrivacyScreen.enable({
android: { dimBackground: true },
ios: { blurEffect: 'dark' }
});
// Navigate to your secure screen
}

// When navigating away from a secure screen
async function navigateAwayFromSecureScreen() {
await PrivacyScreen.disable();
// Navigate to your next screen
}
```

## API

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,13 @@ enum class PrivacyMode {
@CapacitorPlugin(name = "PrivacyScreen")
class PrivacyScreenPlugin : Plugin() {
private var privacyScreenEnabled = false
private var preventScreenshots = false
private var dimBackground = false
private var privacyModeOnActivityHidden = PrivacyMode.NONE
private var dialog: PrivacyScreenDialog? = null

private val recentAppsReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if ((Build.VERSION.SDK_INT >= 31 || usesGestureNavigation(context)) &&
ACTION_CLOSE_SYSTEM_DIALOGS == intent.action) {
if (ACTION_CLOSE_SYSTEM_DIALOGS == intent.action) {
val reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY)
if (reason != null) {
when (reason) {
Expand Down Expand Up @@ -64,19 +62,14 @@ class PrivacyScreenPlugin : Plugin() {
try {
val config = call.getObject("android")
dimBackground = config?.optBoolean("dimBackground") ?: false
preventScreenshots = config?.optBoolean("preventScreenshots") ?: false
privacyModeOnActivityHidden = when (config?.optString("privacyModeOnActivityHidden", "none")) {
"dim" -> PrivacyMode.DIM
"splash" -> PrivacyMode.SPLASH
else -> PrivacyMode.NONE
}

activity.runOnUiThread {
if (preventScreenshots) {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
} else {
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
activity.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
privacyScreenEnabled = true
dialog = PrivacyScreenDialog(activity, dimBackground)

Expand All @@ -94,7 +87,6 @@ class PrivacyScreenPlugin : Plugin() {
@PluginMethod
fun disable(call: PluginCall) {
privacyScreenEnabled = false
preventScreenshots = false
privacyModeOnActivityHidden = PrivacyMode.NONE
activity.runOnUiThread {
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
Expand All @@ -116,17 +108,11 @@ class PrivacyScreenPlugin : Plugin() {
override fun handleOnResume() {
super.handleOnResume()
onRecentAppsTriggered(false)
if (preventScreenshots) {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
} else {
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}

override fun handleOnPause() {
super.handleOnPause()
if (privacyScreenEnabled) {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
when (privacyModeOnActivityHidden) {
PrivacyMode.DIM -> showPrivacyDialog(true)
PrivacyMode.SPLASH -> showPrivacyDialog(false)
Expand All @@ -145,15 +131,17 @@ class PrivacyScreenPlugin : Plugin() {
}

private fun showPrivacyDialog(dim: Boolean) {
if (dialog != null && isDialogViewAttachedToWindowManager()) {
if (dialog?.isShowing == true || isDialogViewAttachedToWindowManager()) {
return
}

if (dialog != null) {
dialog?.dismiss()
dialog = null
}

context.let { context ->
if (context is AppCompatActivity &&
!context.isFinishing &&
dialog?.isShowing != true &&
!isDialogViewAttachedToWindowManager()) {
if (context is AppCompatActivity && !context.isFinishing) {
dialog = PrivacyScreenDialog(context, dim)
dialog?.show()
}
Expand All @@ -164,13 +152,6 @@ class PrivacyScreenPlugin : Plugin() {
return dialog?.window?.decorView?.parent != null
}

@SuppressLint("DiscouragedApi")
private fun usesGestureNavigation(context: Context): Boolean {
val resources = context.resources
val resourceId = resources.getIdentifier("config_navBarInteractionMode", "integer", "android")
return resourceId > 0 && resources.getInteger(resourceId) == 2
}

override fun handleOnDestroy() {
dialog?.dismiss()
dialog = null
Expand Down
8 changes: 4 additions & 4 deletions src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ export interface PrivacyScreenConfig {
dimBackground?: boolean;

/**
* Controls whether screenshots can be taken while the app is in use.
* 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.`
* Note: Privacy screen protection in app switcher is always enabled when the plugin is enabled.
* @default false
* @deprecated This option is no longer necessary. FLAG_SECURE is now always applied when the privacy screen is enabled,
* which prevents screenshots and protects content in the app switcher. This option will be removed in a future version.
*
* If you need to control screenshot prevention separately, you can enable/disable the plugin as needed per screen.
*/
preventScreenshots?: boolean;

Expand Down