@onekeyfe/react-native-capture-protection 0.0.0-stage → 3.0.166

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/ACKNOWLEDGEMENTS.md +18 -0
  2. package/LICENSE +22 -0
  3. package/LICENSE.upstream +20 -0
  4. package/README.md +25 -2
  5. package/ReactNativeCaptureProtection.podspec +37 -0
  6. package/android/CMakeLists.txt +24 -0
  7. package/android/build.gradle +130 -0
  8. package/android/gradle.properties +4 -0
  9. package/android/src/main/AndroidManifest.xml +3 -0
  10. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  11. package/android/src/main/java/com/margelo/nitro/reactnativecaptureprotection/ReactNativeCaptureProtection.kt +285 -0
  12. package/android/src/main/java/com/margelo/nitro/reactnativecaptureprotection/ReactNativeCaptureProtectionPackage.kt +22 -0
  13. package/docs/SPEC.md +117 -0
  14. package/ios/ReactNativeCaptureProtection.swift +251 -0
  15. package/ios/tests/CaptureProtectionTests.swift +50 -0
  16. package/lib/module/ReactNativeCaptureProtection.nitro.js +4 -0
  17. package/lib/module/ReactNativeCaptureProtection.nitro.js.map +1 -0
  18. package/lib/module/index.js +61 -0
  19. package/lib/module/index.js.map +1 -0
  20. package/lib/module/package.json +1 -0
  21. package/lib/typescript/package.json +1 -0
  22. package/lib/typescript/src/ReactNativeCaptureProtection.nitro.d.ts +10 -0
  23. package/lib/typescript/src/ReactNativeCaptureProtection.nitro.d.ts.map +1 -0
  24. package/lib/typescript/src/index.d.ts +22 -0
  25. package/lib/typescript/src/index.d.ts.map +1 -0
  26. package/nitro.json +17 -0
  27. package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
  28. package/nitrogen/generated/android/c++/JHybridReactNativeCaptureProtectionSpec.cpp +88 -0
  29. package/nitrogen/generated/android/c++/JHybridReactNativeCaptureProtectionSpec.hpp +65 -0
  30. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecaptureprotection/Func_void_double.kt +78 -0
  31. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecaptureprotection/HybridReactNativeCaptureProtectionSpec.kt +70 -0
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecaptureprotection/reactnativecaptureprotectionOnLoad.kt +35 -0
  33. package/nitrogen/generated/android/reactnativecaptureprotection+autolinking.cmake +81 -0
  34. package/nitrogen/generated/android/reactnativecaptureprotection+autolinking.gradle +27 -0
  35. package/nitrogen/generated/android/reactnativecaptureprotectionOnLoad.cpp +56 -0
  36. package/nitrogen/generated/android/reactnativecaptureprotectionOnLoad.hpp +34 -0
  37. package/nitrogen/generated/ios/ReactNativeCaptureProtection+autolinking.rb +62 -0
  38. package/nitrogen/generated/ios/ReactNativeCaptureProtection-Swift-Cxx-Bridge.cpp +57 -0
  39. package/nitrogen/generated/ios/ReactNativeCaptureProtection-Swift-Cxx-Bridge.hpp +157 -0
  40. package/nitrogen/generated/ios/ReactNativeCaptureProtection-Swift-Cxx-Umbrella.hpp +46 -0
  41. package/nitrogen/generated/ios/ReactNativeCaptureProtectionAutolinking.mm +33 -0
  42. package/nitrogen/generated/ios/ReactNativeCaptureProtectionAutolinking.swift +26 -0
  43. package/nitrogen/generated/ios/c++/HybridReactNativeCaptureProtectionSpecSwift.cpp +11 -0
  44. package/nitrogen/generated/ios/c++/HybridReactNativeCaptureProtectionSpecSwift.hpp +98 -0
  45. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  46. package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
  47. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  48. package/nitrogen/generated/ios/swift/HybridReactNativeCaptureProtectionSpec.swift +57 -0
  49. package/nitrogen/generated/ios/swift/HybridReactNativeCaptureProtectionSpec_cxx.swift +187 -0
  50. package/nitrogen/generated/shared/c++/HybridReactNativeCaptureProtectionSpec.cpp +23 -0
  51. package/nitrogen/generated/shared/c++/HybridReactNativeCaptureProtectionSpec.hpp +66 -0
  52. package/package.json +173 -4
  53. package/src/ReactNativeCaptureProtection.nitro.ts +8 -0
  54. package/src/index.tsx +65 -0
@@ -0,0 +1,18 @@
1
+ # Acknowledgements
2
+
3
+ This module is adapted from [react-native-capture-protection](https://github.com/wn-na/react-native-capture-protection)
4
+ version **2.3.0**, authored by **lethe / wn-na**. Thank you for the upstream
5
+ screenshot, recording and app-switcher protection implementation.
6
+
7
+ The migration adapts the public event values, iOS secure-text-field layer
8
+ technique and opaque protection windows, and Android FLAG_SECURE and virtual
9
+ display detection approach. It replaces the React Native bridge with Nitro,
10
+ scopes subscriptions by native owner, and removes media-library screenshot
11
+ observation, storage permission requests, custom overlays, providers and hooks.
12
+ OneKey's prior fixes for per-runtime listener ownership and private virtual
13
+ display events are preserved in the adapted implementation.
14
+
15
+ The upstream code is distributed under the MIT license. Its copyright and full
16
+ license are retained in [LICENSE.upstream](LICENSE.upstream). The main [LICENSE](LICENSE)
17
+ also retains the upstream copyright for CocoaPods acknowledgements. Both are included
18
+ in published package contents. Adapted source files identify this notice.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 OneKey
4
+ Copyright (c) 2022 lethe
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 lethe
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,26 @@
1
- # Temporary Holding Version
1
+ # @onekeyfe/react-native-capture-protection
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Permission-minimal capture protection for OneKey iOS and Android.
4
+
5
+ ```ts
6
+ import {
7
+ CaptureProtection,
8
+ CaptureEventType,
9
+ } from '@onekeyfe/react-native-capture-protection';
10
+
11
+ await CaptureProtection.prevent();
12
+ const subscription = CaptureProtection.addListener((event) => {
13
+ if (event === CaptureEventType.CAPTURED) {
14
+ /* Show a warning. */
15
+ }
16
+ });
17
+ subscription.remove();
18
+ await CaptureProtection.allow();
19
+ ```
20
+
21
+ Only the application's existing prevent/allow/listener API is supported.
22
+ No photo-library or filesystem permission is declared or requested.
23
+ Android screenshot notifications require Android 14; older Android versions still
24
+ block capture with FLAG_SECURE. iOS secure-layer behavior requires device testing.
25
+
26
+ See [SPEC](docs/SPEC.md) and [Acknowledgements](ACKNOWLEDGEMENTS.md).
@@ -0,0 +1,37 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "ReactNativeCaptureProtection"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "https://github.com/OneKeyHQ/app-modules/react-native-capture-protection.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = [
17
+ "ios/**/*.{swift}",
18
+ "ios/**/*.{m,mm}",
19
+ "cpp/**/*.{hpp,cpp}",
20
+ ]
21
+
22
+ s.exclude_files = "ios/tests/**/*"
23
+
24
+ s.dependency 'React-jsi'
25
+ s.dependency 'React-callinvoker'
26
+ s.dependency 'ReactNativeNativeLogger'
27
+
28
+ load 'nitrogen/generated/ios/ReactNativeCaptureProtection+autolinking.rb'
29
+ add_nitrogen_files(s)
30
+
31
+ install_modules_dependencies(s)
32
+
33
+ s.test_spec "Tests" do |test_spec|
34
+ test_spec.source_files = "ios/tests/**/*.swift"
35
+ test_spec.requires_app_host = true
36
+ end
37
+ end
@@ -0,0 +1,24 @@
1
+ project(reactnativecaptureprotection)
2
+ cmake_minimum_required(VERSION 3.9.0)
3
+
4
+ set(PACKAGE_NAME reactnativecaptureprotection)
5
+ set(CMAKE_VERBOSE_MAKEFILE ON)
6
+ set(CMAKE_CXX_STANDARD 20)
7
+
8
+ # Define C++ library and add all sources
9
+ add_library(${PACKAGE_NAME} SHARED src/main/cpp/cpp-adapter.cpp)
10
+
11
+ # Add Nitrogen specs :)
12
+ include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/reactnativecaptureprotection+autolinking.cmake)
13
+
14
+ # Set up local includes
15
+ include_directories("src/main/cpp" "../cpp")
16
+
17
+ find_library(LOG_LIB log)
18
+
19
+ # Link all libraries together
20
+ target_link_libraries(
21
+ ${PACKAGE_NAME}
22
+ ${LOG_LIB}
23
+ android # <-- Android core
24
+ )
@@ -0,0 +1,130 @@
1
+ buildscript {
2
+ ext.getExtOrDefault = {name ->
3
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['ReactNativeCaptureProtection_' + name]
4
+ }
5
+
6
+ repositories {
7
+ google()
8
+ mavenCentral()
9
+ }
10
+
11
+ dependencies {
12
+ classpath "com.android.tools.build:gradle:8.7.2"
13
+ // noinspection DifferentKotlinGradleVersion
14
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
15
+ }
16
+ }
17
+
18
+ def reactNativeArchitectures() {
19
+ def value = rootProject.getProperties().get("reactNativeArchitectures")
20
+ return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
21
+ }
22
+
23
+ apply plugin: "com.android.library"
24
+ apply plugin: "kotlin-android"
25
+ apply from: '../nitrogen/generated/android/reactnativecaptureprotection+autolinking.gradle'
26
+
27
+ apply plugin: "com.facebook.react"
28
+
29
+ def getExtOrIntegerDefault(name) {
30
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ReactNativeCaptureProtection_" + name]).toInteger()
31
+ }
32
+
33
+ android {
34
+ namespace "com.margelo.nitro.reactnativecaptureprotection"
35
+
36
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
37
+
38
+ defaultConfig {
39
+ minSdkVersion getExtOrIntegerDefault("minSdkVersion")
40
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
41
+
42
+ externalNativeBuild {
43
+ cmake {
44
+ cppFlags "-frtti -fexceptions -Wall -fstack-protector-all"
45
+ arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
46
+ abiFilters (*reactNativeArchitectures())
47
+
48
+ buildTypes {
49
+ debug {
50
+ cppFlags "-O1 -g"
51
+ }
52
+ release {
53
+ cppFlags "-O2"
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ externalNativeBuild {
61
+ cmake {
62
+ path "CMakeLists.txt"
63
+ }
64
+ }
65
+
66
+ packagingOptions {
67
+ excludes = [
68
+ "META-INF",
69
+ "META-INF/**",
70
+ "**/libc++_shared.so",
71
+ "**/libfbjni.so",
72
+ "**/libjsi.so",
73
+ "**/libfolly_json.so",
74
+ "**/libfolly_runtime.so",
75
+ "**/libglog.so",
76
+ "**/libhermes.so",
77
+ "**/libhermes-executor-debug.so",
78
+ "**/libhermes_executor.so",
79
+ "**/libreactnative.so",
80
+ "**/libreactnativejni.so",
81
+ "**/libturbomodulejsijni.so",
82
+ "**/libreact_nativemodule_core.so",
83
+ "**/libjscexecutor.so"
84
+ ]
85
+ }
86
+
87
+ buildFeatures {
88
+ buildConfig true
89
+ prefab true
90
+ }
91
+
92
+ buildTypes {
93
+ release {
94
+ minifyEnabled false
95
+ }
96
+ }
97
+
98
+ lintOptions {
99
+ disable "GradleCompatible"
100
+ }
101
+
102
+ compileOptions {
103
+ sourceCompatibility JavaVersion.VERSION_1_8
104
+ targetCompatibility JavaVersion.VERSION_1_8
105
+ }
106
+
107
+ sourceSets {
108
+ main {
109
+ java.srcDirs += [
110
+ "generated/java",
111
+ "generated/jni"
112
+ ]
113
+ }
114
+ }
115
+ }
116
+
117
+ repositories {
118
+ mavenCentral()
119
+ google()
120
+ }
121
+
122
+ def kotlin_version = getExtOrDefault("kotlinVersion")
123
+
124
+ dependencies {
125
+ implementation "com.facebook.react:react-android"
126
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
127
+ implementation project(":react-native-nitro-modules")
128
+
129
+ implementation project(":onekeyfe_react-native-native-logger")
130
+ }
@@ -0,0 +1,4 @@
1
+ ReactNativeCaptureProtection_kotlinVersion=1.9.25
2
+ ReactNativeCaptureProtection_compileSdkVersion=35
3
+ ReactNativeCaptureProtection_targetSdkVersion=35
4
+ ReactNativeCaptureProtection_minSdkVersion=24
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.DETECT_SCREEN_CAPTURE" android:minSdkVersion="34" />
3
+ </manifest>
@@ -0,0 +1,6 @@
1
+ #include <jni.h>
2
+ #include "reactnativecaptureprotectionOnLoad.hpp"
3
+
4
+ extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
5
+ return margelo::nitro::reactnativecaptureprotection::initialize(vm);
6
+ }
@@ -0,0 +1,285 @@
1
+ // Adapted from react-native-capture-protection 2.3.0 by lethe; see LICENSE.upstream.
2
+ package com.margelo.nitro.reactnativecaptureprotection
3
+
4
+ import android.app.Activity
5
+ import android.app.Application
6
+ import android.content.Context
7
+ import android.hardware.display.DisplayManager
8
+ import android.os.Build
9
+ import android.os.Bundle
10
+ import android.os.Handler
11
+ import android.os.Looper
12
+ import android.view.Display
13
+ import android.view.WindowManager
14
+ import androidx.annotation.RequiresApi
15
+ import com.facebook.proguard.annotations.DoNotStrip
16
+ import com.facebook.react.ReactApplication
17
+ import com.margelo.nitro.NitroModules
18
+ import com.margelo.nitro.core.Promise
19
+ import java.lang.ref.WeakReference
20
+ import java.util.WeakHashMap
21
+ import java.util.concurrent.FutureTask
22
+ import java.util.concurrent.TimeUnit
23
+ import java.util.concurrent.atomic.AtomicLong
24
+
25
+ @DoNotStrip
26
+ class ReactNativeCaptureProtection : HybridReactNativeCaptureProtectionSpec() {
27
+ private val owner = nextOwner.incrementAndGet()
28
+ private var listener: ((Double) -> Unit)? = null
29
+
30
+ override fun prevent(): Promise<Unit> = onMain {
31
+ val context = NitroModules.applicationContext
32
+ ?: throw IllegalStateException("React context is unavailable.")
33
+ val app = context.applicationContext as Application
34
+ val activity = CaptureProtectionCoordinator.foregroundActivity(app)
35
+ ?: throw IllegalStateException("No foreground activity is available.")
36
+ CaptureProtectionCoordinator.prevent(owner, app, activity)
37
+ }
38
+
39
+ override fun allow(): Promise<Unit> = onMain {
40
+ CaptureProtectionCoordinator.allow(owner)
41
+ }
42
+
43
+ override fun setListener(callback: ((Double) -> Unit)?) {
44
+ mainSync {
45
+ if (callback == null) {
46
+ listener = null
47
+ CaptureProtectionCoordinator.unsubscribe(owner)
48
+ } else {
49
+ val context = NitroModules.applicationContext
50
+ ?: throw IllegalStateException("React context is unavailable.")
51
+ val weak = WeakReference(this)
52
+ val app = context.applicationContext as Application
53
+ CaptureProtectionCoordinator.subscribe(
54
+ owner, app, CaptureProtectionCoordinator.foregroundActivity(app)
55
+ ) { event -> weak.get()?.listener?.invoke(event) }
56
+ listener = callback
57
+ CaptureProtectionCoordinator.emitRecordingState(owner)
58
+ }
59
+ }
60
+ }
61
+
62
+ override fun dispose() {
63
+ release()
64
+ super.dispose()
65
+ }
66
+
67
+ @Suppress("unused")
68
+ protected fun finalize() {
69
+ release()
70
+ }
71
+
72
+ private fun release() {
73
+ mainHandler.post {
74
+ listener = null
75
+ CaptureProtectionCoordinator.unsubscribe(owner)
76
+ CaptureProtectionCoordinator.allow(owner)
77
+ }
78
+ }
79
+
80
+ private fun onMain(operation: () -> Unit): Promise<Unit> {
81
+ val promise = Promise<Unit>()
82
+ mainHandler.post {
83
+ try {
84
+ operation()
85
+ promise.resolve(Unit)
86
+ } catch (error: Exception) {
87
+ promise.reject(error)
88
+ }
89
+ }
90
+ return promise
91
+ }
92
+
93
+ private fun mainSync(operation: () -> Unit) {
94
+ if (Looper.myLooper() == Looper.getMainLooper()) {
95
+ operation()
96
+ } else {
97
+ val task = FutureTask(operation, Unit)
98
+ mainHandler.post(task)
99
+ try {
100
+ task.get(5, TimeUnit.SECONDS)
101
+ } finally {
102
+ task.cancel(false)
103
+ }
104
+ }
105
+ }
106
+
107
+ companion object {
108
+ private val nextOwner = AtomicLong()
109
+ private val mainHandler = Handler(Looper.getMainLooper())
110
+ }
111
+ }
112
+
113
+ private object CaptureProtectionCoordinator : Application.ActivityLifecycleCallbacks,
114
+ DisplayManager.DisplayListener {
115
+ private val handler = Handler(Looper.getMainLooper())
116
+ private val owners = mutableSetOf<Long>()
117
+ private val listeners = mutableMapOf<Long, (Double) -> Unit>()
118
+ private val originalFlags = WeakHashMap<Activity, Boolean>()
119
+ private val displays = mutableSetOf<Int>()
120
+ private var application: Application? = null
121
+ private var displayManager: DisplayManager? = null
122
+ private var currentActivity: WeakReference<Activity>? = null
123
+ private var callbackActivity: WeakReference<Activity>? = null
124
+ private var screenshotCallback: Any? = null
125
+
126
+ fun foregroundActivity(app: Application): Activity? {
127
+ // Nitro's global React context can belong to the background runtime.
128
+ // The application ReactHost is the UI host; lifecycle tracking survives recreation.
129
+ return (app as? ReactApplication)?.reactHost?.currentReactContext?.currentActivity
130
+ ?: currentActivity?.get()
131
+ }
132
+
133
+ private fun checkCapacity(owner: Long) {
134
+ val identities = owners + listeners.keys
135
+ check(owner in identities || identities.size < 32) {
136
+ "Capture protection supports at most 32 native owners."
137
+ }
138
+ }
139
+
140
+ fun prevent(owner: Long, app: Application, activity: Activity) {
141
+ checkCapacity(owner)
142
+ check(!activity.isFinishing && !activity.isDestroyed) { "Activity is unavailable." }
143
+ protect(activity)
144
+ owners.add(owner)
145
+ start(app, activity)
146
+ }
147
+
148
+ fun allow(owner: Long) {
149
+ owners.remove(owner)
150
+ if (owners.isEmpty()) {
151
+ for ((activity, wasSecure) in originalFlags.toMap()) {
152
+ if (!wasSecure) activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
153
+ }
154
+ originalFlags.clear()
155
+ }
156
+ stopIfIdle()
157
+ }
158
+
159
+ fun subscribe(owner: Long, app: Application, activity: Activity?, callback: (Double) -> Unit) {
160
+ checkCapacity(owner)
161
+ listeners[owner] = callback
162
+ start(app, activity)
163
+ }
164
+
165
+ fun unsubscribe(owner: Long) {
166
+ listeners.remove(owner)
167
+ stopIfIdle()
168
+ }
169
+
170
+ fun emitRecordingState(owner: Long) {
171
+ if (displays.isNotEmpty()) listeners[owner]?.invoke(1.0)
172
+ }
173
+
174
+ private fun protect(activity: Activity) {
175
+ if (!originalFlags.containsKey(activity)) {
176
+ originalFlags[activity] = activity.window.attributes.flags and
177
+ WindowManager.LayoutParams.FLAG_SECURE != 0
178
+ }
179
+ activity.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
180
+ }
181
+
182
+ private fun emit(event: Double) {
183
+ for ((owner, callback) in listeners.toMap()) {
184
+ if (listeners.containsKey(owner)) callback(event)
185
+ }
186
+ }
187
+
188
+ private fun start(app: Application, activity: Activity?) {
189
+ if (application == null) {
190
+ application = app
191
+ app.registerActivityLifecycleCallbacks(this)
192
+ displayManager = app.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
193
+ displays.addAll(displayManager!!.displays.map { it.displayId }
194
+ .filter { it != Display.DEFAULT_DISPLAY })
195
+ displayManager!!.registerDisplayListener(this, handler)
196
+ }
197
+ if (activity != null) {
198
+ currentActivity = WeakReference(activity)
199
+ registerScreenshot(activity)
200
+ }
201
+ }
202
+
203
+ private fun stopIfIdle() {
204
+ if (owners.isNotEmpty() || listeners.isNotEmpty()) return
205
+ unregisterScreenshot()
206
+ displayManager?.unregisterDisplayListener(this)
207
+ application?.unregisterActivityLifecycleCallbacks(this)
208
+ displayManager = null
209
+ application = null
210
+ currentActivity = null
211
+ displays.clear()
212
+ }
213
+
214
+ private fun registerScreenshot(activity: Activity) {
215
+ if (Build.VERSION.SDK_INT < 34 || callbackActivity?.get() === activity) return
216
+ unregisterScreenshot()
217
+ try {
218
+ screenshotCallback = ScreenshotApi34.register(activity) { emit(3.0) }
219
+ callbackActivity = WeakReference(activity)
220
+ } catch (_: SecurityException) {
221
+ // A missing normal screenshot-detection permission must not remove protection.
222
+ }
223
+ }
224
+
225
+ private fun unregisterScreenshot() {
226
+ if (Build.VERSION.SDK_INT >= 34) {
227
+ val activity = callbackActivity?.get()
228
+ val callback = screenshotCallback
229
+ if (activity != null && callback != null) {
230
+ try {
231
+ ScreenshotApi34.unregister(activity, callback)
232
+ } catch (_: IllegalArgumentException) {
233
+ // The OS may have already disposed the activity's registration.
234
+ }
235
+ }
236
+ }
237
+ screenshotCallback = null
238
+ callbackActivity = null
239
+ }
240
+
241
+ override fun onActivityResumed(activity: Activity) {
242
+ currentActivity = WeakReference(activity)
243
+ if (owners.isNotEmpty()) protect(activity)
244
+ registerScreenshot(activity)
245
+ }
246
+
247
+ override fun onActivityPaused(activity: Activity) {
248
+ if (callbackActivity?.get() === activity) unregisterScreenshot()
249
+ }
250
+
251
+ override fun onActivityDestroyed(activity: Activity) {
252
+ if (callbackActivity?.get() === activity) unregisterScreenshot()
253
+ if (currentActivity?.get() === activity) currentActivity = null
254
+ originalFlags.remove(activity)
255
+ }
256
+
257
+ override fun onActivityCreated(activity: Activity, state: Bundle?) {}
258
+ override fun onActivityStarted(activity: Activity) {}
259
+ override fun onActivityStopped(activity: Activity) {}
260
+ override fun onActivitySaveInstanceState(activity: Activity, state: Bundle) {}
261
+
262
+ override fun onDisplayAdded(displayId: Int) {
263
+ // Private MediaProjection displays can be absent from getDisplay() here.
264
+ if (displayId != Display.DEFAULT_DISPLAY && displays.add(displayId)) emit(1.0)
265
+ }
266
+
267
+ override fun onDisplayRemoved(displayId: Int) {
268
+ if (displays.remove(displayId) && displays.isEmpty()) emit(2.0)
269
+ }
270
+
271
+ override fun onDisplayChanged(displayId: Int) {}
272
+ }
273
+
274
+ @RequiresApi(34)
275
+ private object ScreenshotApi34 {
276
+ fun register(activity: Activity, onScreenshot: () -> Unit): Any {
277
+ val callback = Activity.ScreenCaptureCallback { onScreenshot() }
278
+ activity.registerScreenCaptureCallback(activity.mainExecutor, callback)
279
+ return callback
280
+ }
281
+
282
+ fun unregister(activity: Activity, callback: Any) {
283
+ activity.unregisterScreenCaptureCallback(callback as Activity.ScreenCaptureCallback)
284
+ }
285
+ }
@@ -0,0 +1,22 @@
1
+ package com.margelo.nitro.reactnativecaptureprotection
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfoProvider
7
+
8
+ class ReactNativeCaptureProtectionPackage : BaseReactPackage() {
9
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
10
+ return null
11
+ }
12
+
13
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
14
+ return ReactModuleInfoProvider { HashMap() }
15
+ }
16
+
17
+ companion object {
18
+ init {
19
+ System.loadLibrary("reactnativecaptureprotection")
20
+ }
21
+ }
22
+ }