@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.
- package/ACKNOWLEDGEMENTS.md +18 -0
- package/LICENSE +22 -0
- package/LICENSE.upstream +20 -0
- package/README.md +25 -2
- package/ReactNativeCaptureProtection.podspec +37 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +130 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/reactnativecaptureprotection/ReactNativeCaptureProtection.kt +285 -0
- package/android/src/main/java/com/margelo/nitro/reactnativecaptureprotection/ReactNativeCaptureProtectionPackage.kt +22 -0
- package/docs/SPEC.md +117 -0
- package/ios/ReactNativeCaptureProtection.swift +251 -0
- package/ios/tests/CaptureProtectionTests.swift +50 -0
- package/lib/module/ReactNativeCaptureProtection.nitro.js +4 -0
- package/lib/module/ReactNativeCaptureProtection.nitro.js.map +1 -0
- package/lib/module/index.js +61 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/ReactNativeCaptureProtection.nitro.d.ts +10 -0
- package/lib/typescript/src/ReactNativeCaptureProtection.nitro.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +22 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +17 -0
- package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JHybridReactNativeCaptureProtectionSpec.cpp +88 -0
- package/nitrogen/generated/android/c++/JHybridReactNativeCaptureProtectionSpec.hpp +65 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecaptureprotection/Func_void_double.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecaptureprotection/HybridReactNativeCaptureProtectionSpec.kt +70 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecaptureprotection/reactnativecaptureprotectionOnLoad.kt +35 -0
- package/nitrogen/generated/android/reactnativecaptureprotection+autolinking.cmake +81 -0
- package/nitrogen/generated/android/reactnativecaptureprotection+autolinking.gradle +27 -0
- package/nitrogen/generated/android/reactnativecaptureprotectionOnLoad.cpp +56 -0
- package/nitrogen/generated/android/reactnativecaptureprotectionOnLoad.hpp +34 -0
- package/nitrogen/generated/ios/ReactNativeCaptureProtection+autolinking.rb +62 -0
- package/nitrogen/generated/ios/ReactNativeCaptureProtection-Swift-Cxx-Bridge.cpp +57 -0
- package/nitrogen/generated/ios/ReactNativeCaptureProtection-Swift-Cxx-Bridge.hpp +157 -0
- package/nitrogen/generated/ios/ReactNativeCaptureProtection-Swift-Cxx-Umbrella.hpp +46 -0
- package/nitrogen/generated/ios/ReactNativeCaptureProtectionAutolinking.mm +33 -0
- package/nitrogen/generated/ios/ReactNativeCaptureProtectionAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeCaptureProtectionSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeCaptureProtectionSpecSwift.hpp +98 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeCaptureProtectionSpec.swift +57 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeCaptureProtectionSpec_cxx.swift +187 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeCaptureProtectionSpec.cpp +23 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeCaptureProtectionSpec.hpp +66 -0
- package/package.json +173 -4
- package/src/ReactNativeCaptureProtection.nitro.ts +8 -0
- 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.
|
package/LICENSE.upstream
ADDED
|
@@ -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
|
-
#
|
|
1
|
+
# @onekeyfe/react-native-capture-protection
|
|
2
2
|
|
|
3
|
-
|
|
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,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
|
+
}
|