Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
lib/
node_modules/
android/build/
*.tgz
PROMOTION.md
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Changelog

## 0.2.0 - 2026-07-20

- Add Android `FLAG_SECURE` capture protection.
- Add best-effort Android content sensitivity controls.
- Detect Galaxy S26 Ultra and Knox Service Plugin Privacy Display readiness.
- Add typed KSP Privacy Display deployment-plan helpers for device-wide and work-profile policies.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Lech Kalinowski and contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
131 changes: 76 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,33 @@
# react-native-private-screen

React Native privacy toolkit for Android with a fast-track API focused on:
Screen privacy for React Native on Android: block capture, mark sensitive content, and prepare Samsung Knox Privacy Display policies for managed Galaxy S26 Ultra devices.

- Screen capture blocking (`FLAG_SECURE`)
- Best-effort content sensitivity (`View.setContentSensitivity(...)` where available)
- Samsung Privacy Display support detection and Settings guidance (no third-party Samsung toggle API assumed)
## What it supports

## Why This Project Exists
- `FLAG_SECURE` screenshot and non-secure display protection
- Best-effort Android content sensitivity
- Galaxy S26 Ultra and Knox Service Plugin readiness detection
- Typed Samsung Privacy Display deployment plans for UEM administrators

Samsung Privacy Display on Galaxy S26 Ultra is a hardware privacy feature, but there is no documented public third-party API (yet) for apps to toggle it programmatically.

This project exists to provide immediate value now:

- expose Android privacy protections apps can use today (`FLAG_SECURE`, best-effort content sensitivity)
- give apps a clean Samsung-specific integration path (support detection + Settings guidance)
- preserve a stable API surface that can later grow into official Samsung Privacy Display controls if Samsung publishes supported APIs

## Scope

This library intentionally separates different privacy controls:

- `FLAG_SECURE`: blocks screenshots and non-secure displays/casting at the window level
- `setContentSensitivity`: best-effort selective screen-sharing privacy on newer Android versions
- Samsung Privacy Display: hardware shoulder-surfing/privacy-angle feature on supported Samsung devices, exposed here only as support detection + Settings navigation until Samsung documents a public app API
Samsung Privacy Display is not a normal app API. Samsung exposes it through Knox Service Plugin 26.05 as an enterprise OEMConfig policy. An app cannot toggle the hardware feature directly; a UEM administrator must deploy the policy.

## Install

```bash
npm install react-native-private-screen
```

Rebuild your Android app after installation.
Rebuild the Android app after installation. iOS is intentionally unsupported; protection calls are no-ops and support detection returns `unavailable`.

## Usage
## Protect sensitive screens

```tsx
import React, { useEffect } from 'react';
import { Button, Text, View } from 'react-native';
import { useEffect } from 'react';
import { PrivateScreen } from 'react-native-private-screen';

export function SensitiveScreen() {
useEffect(() => {
void PrivateScreen.setScreenCaptureProtection('flagSecureOnDemand');
void PrivateScreen.setScreenCaptureProtection('flagSecure');
void PrivateScreen.setContentSensitivity('sensitive');

return () => {
Expand All @@ -50,52 +36,87 @@ export function SensitiveScreen() {
};
}, []);

return (
<View>
<Text>Sensitive content</Text>
<Button
title="Open Samsung display privacy settings"
onPress={() => {
void PrivateScreen.openSamsungPrivacyDisplaySettings();
}}
/>
</View>
);
return null;
}
```

`FLAG_SECURE` blocks screenshots, screen recording, and presentation on non-secure displays at the window level. Android content sensitivity is best-effort and silently does nothing on unsupported OS builds.

## Prepare Samsung Privacy Display

First, inspect whether the current device is eligible and obtain the host application's package name:

```ts
import { PrivateScreen } from 'react-native-private-screen';

const support = await PrivateScreen.getSamsungPrivacyDisplaySupport();

console.log(support.availability);
console.log(support.appPackageName);
console.log(support.device?.kspVersionName);
```

Then generate the values an administrator should map into the Knox Service Plugin managed configuration:

```ts
import {
createSamsungPrivacyDisplayDeploymentPlan,
} from 'react-native-private-screen';

const plan = createSamsungPrivacyDisplayDeploymentPlan({
scope: 'deviceWide',
target: 'selectedApps',
packageNames: ['com.example.securebanking'],
enableCredentialEntry: true,
allowUserToChangePrivacyDisplay: false,
allowUserToChangeApps: false,
allowUserToChangeCredentialEntry: false,
});

console.log(plan.policyPath);
console.log(plan.values);
```

The generated object is a typed deployment guide, not a raw Android Management API payload. KSP's managed-configuration keys are supplied dynamically to compatible UEM consoles, so the administrator should map the returned values in the KSP configuration UI.

### Samsung requirements

- Galaxy S26 Ultra (`SM-S948*`)
- Android 16 and One UI 8.5
- Knox 3.13 or newer
- First One UI 8.5 maintenance release or newer
- Knox Service Plugin 26.05 (`1.5.64`) or newer, deployed through a compatible UEM

`getSamsungPrivacyDisplaySupport()` can verify the manufacturer, model, Android API level, and installed KSP version. It cannot prove that the required One UI maintenance release, Knox version, license, enrollment, or policy assignment is active.

## API

### `PrivateScreen.setScreenCaptureProtection(mode)`

Modes:

- `'off'`
- `'flagSecure'`
- `'flagSecureOnDemand'`
Modes: `'off'`, `'flagSecure'`, and `'flagSecureOnDemand'`. The two secure modes currently apply the same Android window flag; `flagSecureOnDemand` is retained for API compatibility.

### `PrivateScreen.setContentSensitivity(mode)`

Modes:
Modes: `'notSensitive'`, `'sensitive'`, and `'auto'`.

- `'notSensitive'`
- `'sensitive'`
- `'auto'`
### `PrivateScreen.getSamsungPrivacyDisplaySupport()`

On Android versions/builds without `setContentSensitivity`, this call is a no-op.
Returns device eligibility, KSP installation/version information, the host package name, documented requirements, and `canToggleFromApp: false`.

### `PrivateScreen.getSamsungPrivacyDisplaySupport()`
### `PrivateScreen.openSamsungPrivacyDisplaySettings()`

Returns:
Opens the closest documented Android settings screen available. Samsung does not publish a dedicated Privacy Display intent.

- `{ availability: { status: 'unavailable', reason: '...' } }` on non-Samsung/non-Android
- `{ availability: { status: 'unknown', reason: 'No public app API...' } }` on Samsung devices
### `createSamsungPrivacyDisplayDeploymentPlan(options)`

### `PrivateScreen.openSamsungPrivacyDisplaySettings()`
Validates package names and returns a device-wide or work-profile KSP configuration guide for all screens or selected applications.

## Official Samsung documentation

Best-effort navigation to Android settings screens (`Display`, app details, or general Settings) so users can enable Samsung Privacy Display manually when supported by their device/firmware.
- [Knox Service Plugin 26.05 release notes](https://docs.samsungknox.com/admin/knox-platform-for-enterprise/knox-service-plugin/release-notes/26-05/)
- [Advanced Restriction policies](https://docs.samsungknox.com/admin/knox-platform-for-enterprise/knox-service-plugin/configure-advanced-policies/advanced-restriction-policies/)
- [Knox Service Plugin managed configurations](https://docs.samsungknox.com/dev/managed-configurations/knox-service-plugin/)

## Notes
## License

- This library does **not** claim to programmatically toggle Samsung Privacy Display.
- Samsung-specific programmatic controls should be added only after an official public API/intent contract is documented and verified on hardware.
MIT
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,5 @@ repositories {
}

dependencies {
implementation "com.facebook.react:react-native:+"
implementation "com.facebook.react:react-android:+"
}
6 changes: 5 additions & 1 deletion android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
<manifest package="com.rnprivatescreen" />
<manifest package="com.rnprivatescreen">
<queries>
<package android:name="com.samsung.android.knox.kpu" />
</queries>
</manifest>
124 changes: 118 additions & 6 deletions android/src/main/java/com/rnprivatescreen/RNPrivateScreenModule.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.rnprivatescreen

import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.provider.Settings
Expand All @@ -15,6 +17,7 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.UiThreadUtil
import java.lang.reflect.Method
import java.util.Locale

class RNPrivateScreenModule(
reactContext: ReactApplicationContext
Expand Down Expand Up @@ -84,18 +87,76 @@ class RNPrivateScreenModule(
fun getSamsungPrivacyDisplaySupport(promise: Promise) {
try {
val isSamsung = Build.MANUFACTURER.equals("samsung", ignoreCase = true)
val isGalaxyS26Ultra = Build.MODEL.uppercase(Locale.US).startsWith("SM-S948")
val hasAndroid16 = Build.VERSION.SDK_INT >= MINIMUM_ANDROID_API_LEVEL
val kspPackageInfo = getPackageInfo(KNOX_SERVICE_PLUGIN_PACKAGE_NAME)
val kspVersionName = kspPackageInfo?.versionName
val hasSupportedKspVersion = isVersionAtLeast(
kspVersionName,
MINIMUM_KSP_VERSION
)
val result = Arguments.createMap()
val availability = Arguments.createMap()

if (!isSamsung) {
availability.putString("status", "unavailable")
availability.putString("reason", "non-Samsung device")
} else {
availability.putString("status", "unknown")
availability.putString("reason", "No public app API is available; use user Settings flow")
when {
!isSamsung -> {
availability.putString("status", "unavailable")
availability.putString("reason", "Privacy Display requires a Samsung device")
}
!isGalaxyS26Ultra -> {
availability.putString("status", "unavailable")
availability.putString("reason", "Privacy Display policy is supported only on Galaxy S26 Ultra")
}
!hasAndroid16 -> {
availability.putString("status", "unavailable")
availability.putString("reason", "Privacy Display policy requires Android 16 / One UI 8.5")
}
kspPackageInfo == null -> {
availability.putString("status", "unknown")
availability.putString(
"reason",
"Eligible hardware detected; Knox Service Plugin 26.05+ must be deployed by a UEM"
)
}
hasSupportedKspVersion == false -> {
availability.putString("status", "unavailable")
availability.putString("reason", "Knox Service Plugin 1.5.64 or newer is required")
}
hasSupportedKspVersion == null -> {
availability.putString("status", "unknown")
availability.putString("reason", "Unable to verify the Knox Service Plugin version")
}
else -> {
availability.putString("status", "available")
availability.putString(
"reason",
"Eligible for enterprise policy; a UEM admin must configure Privacy Display"
)
}
}

result.putMap("availability", availability)
result.putString("delivery", "knoxServicePlugin")
result.putBoolean("canToggleFromApp", false)
result.putString("appPackageName", reactApplicationContext.packageName)

val device = Arguments.createMap()
device.putString("manufacturer", Build.MANUFACTURER)
device.putString("model", Build.MODEL)
device.putInt("androidApiLevel", Build.VERSION.SDK_INT)
device.putBoolean("kspInstalled", kspPackageInfo != null)
kspVersionName?.let { device.putString("kspVersionName", it) }
result.putMap("device", device)

val requirements = Arguments.createMap()
requirements.putString("supportedModel", "Galaxy S26 Ultra (SM-S948*)")
requirements.putInt("minimumAndroidApiLevel", MINIMUM_ANDROID_API_LEVEL)
requirements.putString("minimumOneUiVersion", "8.5")
requirements.putString("minimumKnoxVersion", "3.13")
requirements.putString("minimumKspVersion", MINIMUM_KSP_VERSION)
requirements.putBoolean("maintenanceReleaseRequired", true)
result.putMap("requirements", requirements)

promise.resolve(result)
} catch (t: Throwable) {
promise.reject("E_INTERNAL", t.message, t)
Expand Down Expand Up @@ -200,4 +261,55 @@ class RNPrivateScreenModule(
null
}
}

@Suppress("DEPRECATION")
private fun getPackageInfo(packageName: String): PackageInfo? {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
reactApplicationContext.packageManager.getPackageInfo(
packageName,
PackageManager.PackageInfoFlags.of(0)
)
} else {
reactApplicationContext.packageManager.getPackageInfo(packageName, 0)
}
} catch (_: PackageManager.NameNotFoundException) {
null
}
}

private fun isVersionAtLeast(versionName: String?, minimumVersion: String): Boolean? {
if (versionName == null) {
return null
}

val current = VERSION_NUMBER_PATTERN.findAll(versionName)
.map { it.value.toInt() }
.toList()
val minimum = VERSION_NUMBER_PATTERN.findAll(minimumVersion)
.map { it.value.toInt() }
.toList()

if (current.isEmpty() || minimum.isEmpty()) {
return null
}

val partCount = maxOf(current.size, minimum.size)
for (index in 0 until partCount) {
val currentPart = current.getOrElse(index) { 0 }
val minimumPart = minimum.getOrElse(index) { 0 }
if (currentPart != minimumPart) {
return currentPart > minimumPart
}
}

return true
}

companion object {
private const val KNOX_SERVICE_PLUGIN_PACKAGE_NAME = "com.samsung.android.knox.kpu"
private const val MINIMUM_ANDROID_API_LEVEL = 36
private const val MINIMUM_KSP_VERSION = "1.5.64"
private val VERSION_NUMBER_PATTERN = Regex("\\d+")
}
}
Loading