Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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. Note: Make sure to call the appropriate method whenever navigating between screens, including when using back navigation.

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

// Enable 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
}

// Disable when navigating to a non-secure screen
async function navigateToPublicScreen() {
await PrivacyScreen.disable();
// Navigate to your public 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
66 changes: 33 additions & 33 deletions example-app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions example-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
"lint": "eslint"
},
"dependencies": {
"@capacitor/app": "next",
"@capacitor/core": "next",
"@capacitor/haptics": "next",
"@capacitor/keyboard": "next",
"@capacitor/app": "^7.0.0",
"@capacitor/core": "^7.0.0",
"@capacitor/haptics": "^7.0.0",
"@capacitor/keyboard": "^7.0.0",
"@capacitor/privacy-screen": "file:../",
"@capacitor/status-bar": "next",
"@capacitor/status-bar": "^7.0.0",
"@ionic-enterprise/identity-vault": "latest",
"@ionic/react": "^8.0.0",
"@ionic/react-router": "^8.0.0",
Expand All @@ -28,7 +28,7 @@
"react-router-dom": "^5.3.4"
},
"devDependencies": {
"@capacitor/cli": "next",
"@capacitor/cli": "^7.0.0",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@vitejs/plugin-legacy": "^5.0.0",
Expand Down
Loading