catalyst-core 0.1.0-canary.2 → 0.1.0-canary.3

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 (16) hide show
  1. package/dist/native/androidProject/app/build.gradle.kts +12 -4
  2. package/dist/native/androidProject/app/proguard-rules.pro +25 -0
  3. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +10 -0
  4. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/design/DesignTokens.kt +307 -0
  5. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/EmulatorDetector.kt +203 -0
  6. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/FridaDetector.kt +187 -0
  7. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityAlertHandler.kt +170 -0
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityAlertUI.kt +366 -0
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityBottomSheet.kt +138 -0
  10. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityCheckManager.kt +511 -0
  11. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityCheckScheduler.kt +68 -0
  12. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/DeviceInfoUtils.kt +12 -0
  13. package/dist/native/androidProject/app/src/main/res/values/themes.xml +12 -0
  14. package/package.json +1 -1
  15. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/LegacyGoogleSignInPlugin.kt +0 -176
  16. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/README.md +0 -97
@@ -114,12 +114,12 @@ android {
114
114
  }
115
115
 
116
116
  compileOptions {
117
- sourceCompatibility = JavaVersion.VERSION_1_8
118
- targetCompatibility = JavaVersion.VERSION_1_8
117
+ sourceCompatibility = JavaVersion.VERSION_11
118
+ targetCompatibility = JavaVersion.VERSION_11
119
119
  }
120
120
 
121
121
  kotlinOptions {
122
- jvmTarget = "1.8"
122
+ jvmTarget = "11"
123
123
  }
124
124
 
125
125
  buildFeatures {
@@ -136,6 +136,10 @@ android {
136
136
  excludes.add("META-INF/INDEX.LIST")
137
137
  excludes.add("META-INF/io.netty.versions.properties")
138
138
  }
139
+ jniLibs {
140
+ // Enable 16KB page size alignment for all native libraries
141
+ useLegacyPackaging = false
142
+ }
139
143
  }
140
144
 
141
145
  // Configure lint options
@@ -168,13 +172,17 @@ dependencies {
168
172
  androidTestImplementation(libs.androidx.espresso.core)
169
173
  implementation(libs.androidx.webkit)
170
174
  implementation("org.json:json:20231013")
171
-
175
+
172
176
  // Ktor Server dependencies for FrameworkServer (~200KB total)
173
177
  implementation("io.ktor:ktor-server-core:3.0.3")
174
178
  implementation("io.ktor:ktor-server-netty:3.0.3")
175
179
  implementation("io.ktor:ktor-server-content-negotiation:3.0.3")
176
180
  implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
177
181
 
182
+ // Security detection dependencies
183
+ implementation("com.scottyab:rootbeer-lib:0.1.1") // Root detection
184
+ implementation("com.google.android.play:integrity:1.3.0") // Play Integrity API
185
+
178
186
  // SLF4J simple logger for Ktor (optional, can be excluded if needed)
179
187
  implementation("org.slf4j:slf4j-simple:2.0.9")
180
188
 
@@ -59,6 +59,31 @@
59
59
  -keep class io.yourname.androidproject.WebCacheManager$CacheEntry { *; }
60
60
  -keep class io.yourname.androidproject.WebCacheManager$CacheMetadata { *; }
61
61
 
62
+ # Keep RootBeer library for root detection
63
+ -keep class com.scottyab.rootbeer.** { *; }
64
+ -dontwarn com.scottyab.rootbeer.**
65
+
66
+ # Security: keep only the public API surface that MainActivity wires up.
67
+ # Detection internals (EmulatorDetector, FridaDetector) and UI classes
68
+ # (SecurityBottomSheet, SecurityAlertUI) are intentionally obfuscated.
69
+ -keep class io.yourname.androidproject.security.SecurityCheckScheduler {
70
+ public static void initialize(android.content.Context, kotlinx.coroutines.CoroutineScope, io.yourname.androidproject.security.SecurityCheckScheduler$SecurityCheckCallback);
71
+ }
72
+ -keep interface io.yourname.androidproject.security.SecurityCheckScheduler$SecurityCheckCallback {
73
+ public void onSecurityCheckComplete(org.json.JSONObject);
74
+ }
75
+ -keep class io.yourname.androidproject.security.SecurityCheckManager {
76
+ public static org.json.JSONObject getLatestSecurityResults(android.content.Context);
77
+ public static java.lang.String getSecurityMode(android.content.Context);
78
+ }
79
+ -keep class io.yourname.androidproject.security.SecurityAlertHandler {
80
+ public static void handleSecurityCheckResults(android.app.Activity, org.json.JSONObject);
81
+ }
82
+
83
+ # Keep Play Integrity API
84
+ -keep class com.google.android.play.core.integrity.** { *; }
85
+ -dontwarn com.google.android.play.core.integrity.**
86
+
62
87
  # Remove debug logging in release builds
63
88
  -assumenosideeffects class android.util.Log {
64
89
  public static *** d(...);
@@ -20,6 +20,7 @@ import io.yourname.androidproject.utils.NetworkUtils
20
20
  import io.yourname.androidproject.utils.NotificationConstants
21
21
  import io.yourname.androidproject.utils.SafeAreaInsets
22
22
  import io.yourname.androidproject.utils.SafeAreaUtils
23
+ import io.yourname.androidproject.security.SecurityCheckScheduler
23
24
  import kotlinx.coroutines.CoroutineScope
24
25
  import kotlinx.coroutines.MainScope
25
26
  import kotlinx.coroutines.cancelChildren
@@ -253,6 +254,15 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
253
254
 
254
255
  configureEdgeToEdge()
255
256
 
257
+ // Initialize security checks (release builds only)
258
+ if (!BuildConfig.DEBUG) {
259
+ SecurityCheckScheduler.initialize(this, this, object : SecurityCheckScheduler.SecurityCheckCallback {
260
+ override fun onSecurityCheckComplete(results: JSONObject) {
261
+ io.yourname.androidproject.security.SecurityAlertHandler.handleSecurityCheckResults(this@MainActivity, results)
262
+ }
263
+ })
264
+ }
265
+
256
266
  // Initialize MetricsMonitor
257
267
  metricsMonitor = MetricsMonitor.getInstance(this)
258
268
 
@@ -0,0 +1,307 @@
1
+ package io.yourname.androidproject.design
2
+
3
+ import android.content.Context
4
+ import android.content.res.Configuration
5
+ import androidx.annotation.ColorInt
6
+
7
+ /**
8
+ * Global design tokens for the application.
9
+ * Follows Material Design 3 principles with support for light/dark themes.
10
+ *
11
+ * All spacing follows 8dp grid system.
12
+ * Color system supports dynamic theming based on system configuration.
13
+ */
14
+ object DesignTokens {
15
+
16
+ // ============================================================
17
+ // COLORS - Light Theme
18
+ // ============================================================
19
+ object LightColors {
20
+ // Surface colors
21
+ @ColorInt const val SURFACE = 0xFFFFFFFF.toInt() // Pure white
22
+ @ColorInt const val SURFACE_VARIANT = 0xFFF5F5F5.toInt() // Light gray background
23
+
24
+ // Error colors (for alerts, warnings)
25
+ @ColorInt const val ERROR = 0xFFB3261E.toInt() // Material error red
26
+ @ColorInt const val ERROR_CONTAINER = 0xFFF9DEDC.toInt() // Light red background
27
+ @ColorInt const val ON_ERROR = 0xFFFFFFFF.toInt() // White text on error
28
+ @ColorInt const val ON_ERROR_CONTAINER = 0xFF410E0B.toInt() // Dark red text
29
+
30
+ // Text colors
31
+ @ColorInt const val ON_SURFACE = 0xFF1C1B1F.toInt() // Primary text (near black)
32
+ @ColorInt const val ON_SURFACE_VARIANT = 0xFF49454F.toInt() // Secondary text (gray)
33
+ @ColorInt const val OUTLINE = 0xFF79747E.toInt() // Dividers, borders
34
+
35
+ // Accent colors
36
+ @ColorInt const val PRIMARY = 0xFF6750A4.toInt() // Material purple
37
+ @ColorInt const val ON_PRIMARY = 0xFFFFFFFF.toInt() // White text on primary
38
+ @ColorInt const val PRIMARY_CONTAINER = 0xFFEADDFF.toInt() // Light purple background
39
+ @ColorInt const val ON_PRIMARY_CONTAINER = 0xFF21005D.toInt() // Dark purple text
40
+
41
+ // Secondary colors
42
+ @ColorInt const val SECONDARY = 0xFF625B71.toInt() // Material gray-purple
43
+ @ColorInt const val ON_SECONDARY = 0xFFFFFFFF.toInt() // White text on secondary
44
+ @ColorInt const val SECONDARY_CONTAINER = 0xFFE8DEF8.toInt() // Light secondary background
45
+ @ColorInt const val ON_SECONDARY_CONTAINER = 0xFF1D192B.toInt() // Dark text
46
+ }
47
+
48
+ // ============================================================
49
+ // COLORS - Dark Theme
50
+ // ============================================================
51
+ object DarkColors {
52
+ // Surface colors
53
+ @ColorInt const val SURFACE = 0xFF1C1B1F.toInt() // Dark surface
54
+ @ColorInt const val SURFACE_VARIANT = 0xFF2B2930.toInt() // Darker variant
55
+
56
+ // Error colors (for alerts, warnings)
57
+ @ColorInt const val ERROR = 0xFFF2B8B5.toInt() // Light red for dark mode
58
+ @ColorInt const val ERROR_CONTAINER = 0xFF8C1D18.toInt() // Dark red background
59
+ @ColorInt const val ON_ERROR = 0xFF601410.toInt() // Dark text on light error
60
+ @ColorInt const val ON_ERROR_CONTAINER = 0xFFF2B8B5.toInt() // Light text on dark error
61
+
62
+ // Text colors
63
+ @ColorInt const val ON_SURFACE = 0xFFE6E1E5.toInt() // Light text
64
+ @ColorInt const val ON_SURFACE_VARIANT = 0xFFCAC4D0.toInt() // Secondary light text
65
+ @ColorInt const val OUTLINE = 0xFF938F99.toInt() // Light dividers, borders
66
+
67
+ // Accent colors
68
+ @ColorInt const val PRIMARY = 0xFFD0BCFF.toInt() // Light purple for dark mode
69
+ @ColorInt const val ON_PRIMARY = 0xFF381E72.toInt() // Dark text on light primary
70
+ @ColorInt const val PRIMARY_CONTAINER = 0xFF4F378B.toInt() // Dark purple background
71
+ @ColorInt const val ON_PRIMARY_CONTAINER = 0xFFEADDFF.toInt() // Light text
72
+
73
+ // Secondary colors
74
+ @ColorInt const val SECONDARY = 0xFFCCC2DC.toInt() // Light gray-purple for dark
75
+ @ColorInt const val ON_SECONDARY = 0xFF332D41.toInt() // Dark text on light secondary
76
+ @ColorInt const val SECONDARY_CONTAINER = 0xFF4A4458.toInt() // Dark secondary background
77
+ @ColorInt const val ON_SECONDARY_CONTAINER = 0xFFE8DEF8.toInt() // Light text
78
+ }
79
+
80
+ // ============================================================
81
+ // SPACING - Following 8dp Grid System
82
+ // ============================================================
83
+ object Spacing {
84
+ const val SPACE_XXS = 4 // 4dp - Minimal spacing
85
+ const val SPACE_XS = 8 // 8dp - Tight spacing
86
+ const val SPACE_SM = 12 // 12dp - Small spacing
87
+ const val SPACE_MD = 16 // 16dp - Medium spacing (default)
88
+ const val SPACE_LG = 24 // 24dp - Large spacing
89
+ const val SPACE_XL = 32 // 32dp - Extra large spacing
90
+ const val SPACE_XXL = 48 // 48dp - Maximum spacing
91
+ }
92
+
93
+ // ============================================================
94
+ // DIMENSIONS
95
+ // ============================================================
96
+ object Dimensions {
97
+ // Corner radius
98
+ const val CORNER_RADIUS_SM = 8 // Small corners (buttons, chips)
99
+ const val CORNER_RADIUS_MD = 16 // Medium corners (cards)
100
+ const val CORNER_RADIUS_LG = 24 // Large corners (bottom sheet top)
101
+ const val CORNER_RADIUS_XL = 28 // Extra large (full rounded)
102
+
103
+ // Icon sizes
104
+ const val ICON_SIZE_SM = 24 // Small icons
105
+ const val ICON_SIZE_MD = 48 // Medium icons
106
+ const val ICON_SIZE_LG = 64 // Large icons (hero icon)
107
+ const val ICON_SIZE_XL = 96 // Extra large icons
108
+
109
+ // Minimum touch target (Material Design)
110
+ const val MIN_TOUCH_TARGET = 48 // 48dp minimum for accessibility
111
+
112
+ // Elevation (for shadows/depth)
113
+ const val ELEVATION_NONE = 0f
114
+ const val ELEVATION_SM = 2f // Small elevation
115
+ const val ELEVATION_MD = 4f // Medium elevation
116
+ const val ELEVATION_LG = 8f // Large elevation
117
+ }
118
+
119
+ // ============================================================
120
+ // TYPOGRAPHY
121
+ // ============================================================
122
+ object Typography {
123
+ // Text sizes (SP - scalable pixels)
124
+ const val TEXT_SIZE_DISPLAY = 32f // Large headlines
125
+ const val TEXT_SIZE_HEADLINE = 24f // Section headlines
126
+ const val TEXT_SIZE_TITLE = 20f // Card titles
127
+ const val TEXT_SIZE_BODY_LG = 16f // Large body text
128
+ const val TEXT_SIZE_BODY = 14f // Default body text
129
+ const val TEXT_SIZE_LABEL = 12f // Small labels, captions
130
+
131
+ // Line height multipliers
132
+ const val LINE_HEIGHT_TIGHT = 1.2f // Tight line height
133
+ const val LINE_HEIGHT_NORMAL = 1.5f // Normal line height
134
+ const val LINE_HEIGHT_RELAXED = 1.7f // Relaxed line height
135
+
136
+ // Font weights (Android Typeface)
137
+ const val FONT_WEIGHT_NORMAL = android.graphics.Typeface.NORMAL
138
+ const val FONT_WEIGHT_BOLD = android.graphics.Typeface.BOLD
139
+ }
140
+
141
+ // ============================================================
142
+ // ANIMATION DURATIONS (milliseconds)
143
+ // ============================================================
144
+ object Animation {
145
+ const val DURATION_SHORT = 200L // Quick transitions
146
+ const val DURATION_MEDIUM = 300L // Standard transitions
147
+ const val DURATION_LONG = 500L // Emphasized transitions
148
+ const val DURATION_EXTRA_LONG = 700L // Hero transitions
149
+ }
150
+
151
+ // ============================================================
152
+ // OPACITY/ALPHA VALUES
153
+ // ============================================================
154
+ object Opacity {
155
+ const val DISABLED = 0.38f // Disabled state
156
+ const val MEDIUM = 0.60f // Medium emphasis
157
+ const val HIGH = 0.87f // High emphasis
158
+ const val FULL = 1.0f // Full opacity
159
+ }
160
+
161
+ // ============================================================
162
+ // HELPER FUNCTIONS
163
+ // ============================================================
164
+
165
+ /**
166
+ * Check if the device is in dark mode.
167
+ *
168
+ * @param context Android context
169
+ * @return true if dark mode is enabled
170
+ */
171
+ fun isDarkMode(context: Context): Boolean {
172
+ val nightModeFlags = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
173
+ return nightModeFlags == Configuration.UI_MODE_NIGHT_YES
174
+ }
175
+
176
+ /**
177
+ * Get surface color based on current theme.
178
+ */
179
+ @ColorInt
180
+ fun getSurfaceColor(context: Context): Int {
181
+ return if (isDarkMode(context)) DarkColors.SURFACE else LightColors.SURFACE
182
+ }
183
+
184
+ /**
185
+ * Get surface variant color based on current theme.
186
+ */
187
+ @ColorInt
188
+ fun getSurfaceVariantColor(context: Context): Int {
189
+ return if (isDarkMode(context)) DarkColors.SURFACE_VARIANT else LightColors.SURFACE_VARIANT
190
+ }
191
+
192
+ /**
193
+ * Get error color based on current theme.
194
+ */
195
+ @ColorInt
196
+ fun getErrorColor(context: Context): Int {
197
+ return if (isDarkMode(context)) DarkColors.ERROR else LightColors.ERROR
198
+ }
199
+
200
+ /**
201
+ * Get error container color based on current theme.
202
+ */
203
+ @ColorInt
204
+ fun getErrorContainerColor(context: Context): Int {
205
+ return if (isDarkMode(context)) DarkColors.ERROR_CONTAINER else LightColors.ERROR_CONTAINER
206
+ }
207
+
208
+ /**
209
+ * Get on-error color (text on error background) based on current theme.
210
+ */
211
+ @ColorInt
212
+ fun getOnErrorColor(context: Context): Int {
213
+ return if (isDarkMode(context)) DarkColors.ON_ERROR else LightColors.ON_ERROR
214
+ }
215
+
216
+ /**
217
+ * Get on-error-container color based on current theme.
218
+ */
219
+ @ColorInt
220
+ fun getOnErrorContainerColor(context: Context): Int {
221
+ return if (isDarkMode(context)) DarkColors.ON_ERROR_CONTAINER else LightColors.ON_ERROR_CONTAINER
222
+ }
223
+
224
+ /**
225
+ * Get primary text color based on current theme.
226
+ */
227
+ @ColorInt
228
+ fun getOnSurfaceColor(context: Context): Int {
229
+ return if (isDarkMode(context)) DarkColors.ON_SURFACE else LightColors.ON_SURFACE
230
+ }
231
+
232
+ /**
233
+ * Get secondary text color based on current theme.
234
+ */
235
+ @ColorInt
236
+ fun getOnSurfaceVariantColor(context: Context): Int {
237
+ return if (isDarkMode(context)) DarkColors.ON_SURFACE_VARIANT else LightColors.ON_SURFACE_VARIANT
238
+ }
239
+
240
+ /**
241
+ * Get outline color (dividers, borders) based on current theme.
242
+ */
243
+ @ColorInt
244
+ fun getOutlineColor(context: Context): Int {
245
+ return if (isDarkMode(context)) DarkColors.OUTLINE else LightColors.OUTLINE
246
+ }
247
+
248
+ /**
249
+ * Get primary color based on current theme.
250
+ */
251
+ @ColorInt
252
+ fun getPrimaryColor(context: Context): Int {
253
+ return if (isDarkMode(context)) DarkColors.PRIMARY else LightColors.PRIMARY
254
+ }
255
+
256
+ /**
257
+ * Get on-primary color (text on primary background) based on current theme.
258
+ */
259
+ @ColorInt
260
+ fun getOnPrimaryColor(context: Context): Int {
261
+ return if (isDarkMode(context)) DarkColors.ON_PRIMARY else LightColors.ON_PRIMARY
262
+ }
263
+
264
+ /**
265
+ * Get primary container color based on current theme.
266
+ */
267
+ @ColorInt
268
+ fun getPrimaryContainerColor(context: Context): Int {
269
+ return if (isDarkMode(context)) DarkColors.PRIMARY_CONTAINER else LightColors.PRIMARY_CONTAINER
270
+ }
271
+
272
+ /**
273
+ * Get on-primary-container color based on current theme.
274
+ */
275
+ @ColorInt
276
+ fun getOnPrimaryContainerColor(context: Context): Int {
277
+ return if (isDarkMode(context)) DarkColors.ON_PRIMARY_CONTAINER else LightColors.ON_PRIMARY_CONTAINER
278
+ }
279
+
280
+ /**
281
+ * Get secondary color based on current theme.
282
+ */
283
+ @ColorInt
284
+ fun getSecondaryColor(context: Context): Int {
285
+ return if (isDarkMode(context)) DarkColors.SECONDARY else LightColors.SECONDARY
286
+ }
287
+
288
+ /**
289
+ * Get on-secondary color based on current theme.
290
+ */
291
+ @ColorInt
292
+ fun getOnSecondaryColor(context: Context): Int {
293
+ return if (isDarkMode(context)) DarkColors.ON_SECONDARY else LightColors.ON_SECONDARY
294
+ }
295
+
296
+ /**
297
+ * Convert DP to pixels.
298
+ *
299
+ * @param context Android context
300
+ * @param dp Value in density-independent pixels
301
+ * @return Value in pixels
302
+ */
303
+ fun dpToPx(context: Context, dp: Int): Int {
304
+ return (dp * context.resources.displayMetrics.density).toInt()
305
+ }
306
+
307
+ }
@@ -0,0 +1,203 @@
1
+ package io.yourname.androidproject.security
2
+
3
+ import android.content.Context
4
+ import android.hardware.Sensor
5
+ import android.hardware.SensorManager
6
+ import android.os.Build
7
+ import io.yourname.androidproject.utils.BridgeUtils
8
+
9
+ /**
10
+ * Local emulator detection based on Build properties and hardware features.
11
+ * No external telemetry or logging to third parties.
12
+ */
13
+ object EmulatorDetector {
14
+ private const val TAG = "EmulatorDetector"
15
+
16
+ // Known emulator indicators
17
+ private val EMULATOR_PROPERTIES = setOf(
18
+ "sdk", "google_sdk", "sdk_gphone", "sdk_google",
19
+ "vbox", "goldfish", "ranchu", "generic"
20
+ )
21
+
22
+ // Whitelist: Known legitimate patterns that contain 'sdk' but are NOT emulators
23
+ // These are excluded from emulator detection to prevent false positives
24
+ private val LEGITIMATE_SDK_PATTERNS = setOf(
25
+ "mt", "mediatek", "helio", "qcom", "qualcomm", "snapdragon"
26
+ )
27
+
28
+ private val SUSPICIOUS_MANUFACTURERS = setOf(
29
+ "Genymotion", "unknown", "Generic"
30
+ )
31
+
32
+ private val SUSPICIOUS_MODELS = setOf(
33
+ "sdk", "google_sdk", "Android SDK built for x86",
34
+ "Android SDK built for arm", "Emulator"
35
+ )
36
+
37
+ /**
38
+ * Performs multiple checks to detect if running on an emulator.
39
+ * Returns true if device shows emulator characteristics.
40
+ */
41
+ fun isEmulator(context: Context): Boolean {
42
+ try {
43
+ val buildCheck = checkBuildProperties()
44
+ val hardwareCheck = checkHardwareFeatures(context)
45
+ val fingerprintCheck = checkFingerprint()
46
+
47
+ val checks = mapOf("Build" to buildCheck, "Hardware" to hardwareCheck, "Fingerprint" to fingerprintCheck)
48
+ val isEmulator = checks.values.any { it }
49
+
50
+ val fired = checks.filter { it.value }.keys
51
+ BridgeUtils.logDebug(TAG, "Emulator check results: $checks — fired: $fired")
52
+ BridgeUtils.logDebug(TAG, "Build info: FINGERPRINT=${Build.FINGERPRINT}, MODEL=${Build.MODEL}, MANUFACTURER=${Build.MANUFACTURER}, HARDWARE=${Build.HARDWARE}, DEVICE=${Build.DEVICE}, PRODUCT=${Build.PRODUCT}")
53
+
54
+ if (isEmulator) {
55
+ BridgeUtils.logDebug(TAG, "EMULATOR DETECTED: $fired")
56
+ }
57
+
58
+ return isEmulator
59
+ } catch (e: Exception) {
60
+ BridgeUtils.logError(TAG, "Error during emulator detection: ${e.message}", e)
61
+ return false
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Check Build properties for emulator indicators
67
+ */
68
+ private fun checkBuildProperties(): Boolean {
69
+ val fingerprint = Build.FINGERPRINT.lowercase()
70
+ val model = Build.MODEL.lowercase()
71
+ val manufacturer = Build.MANUFACTURER.lowercase()
72
+ val brand = Build.BRAND.lowercase()
73
+ val device = Build.DEVICE.lowercase()
74
+ val product = Build.PRODUCT.lowercase()
75
+ val hardware = Build.HARDWARE.lowercase()
76
+
77
+ BridgeUtils.logDebug(TAG, "Build properties - fingerprint: $fingerprint, model: $model, manufacturer: $manufacturer")
78
+ BridgeUtils.logDebug(TAG, "Build properties - brand: $brand, device: $device, product: $product, hardware: $hardware")
79
+
80
+ // Check for emulator-specific strings in Build properties
81
+ var sdkMatchFound = false
82
+ val hasEmulatorProperty = EMULATOR_PROPERTIES.any {
83
+ val found = fingerprint.contains(it) ||
84
+ model.contains(it) ||
85
+ manufacturer.contains(it) ||
86
+ brand.contains(it) ||
87
+ device.contains(it) ||
88
+ product.contains(it) ||
89
+ hardware.contains(it)
90
+ if (found) {
91
+ BridgeUtils.logDebug(TAG, "Found emulator property: '$it'")
92
+ // Track if 'sdk' was the match (for whitelist check)
93
+ if (it == "sdk") {
94
+ sdkMatchFound = true
95
+ }
96
+ }
97
+ found
98
+ }
99
+
100
+ // If flagged as emulator ONLY because of 'sdk', check if it's a legitimate device
101
+ val isLegitimateDevice = if (sdkMatchFound && hasEmulatorProperty) {
102
+ val isLegit = LEGITIMATE_SDK_PATTERNS.any {
103
+ manufacturer.contains(it) ||
104
+ brand.contains(it) ||
105
+ hardware.contains(it) ||
106
+ model.contains(it)
107
+ }
108
+ if (isLegit) {
109
+ BridgeUtils.logDebug(TAG, "Device flagged by 'sdk' but matches legitimate pattern - likely MediaTek/Qualcomm chipset")
110
+ }
111
+ isLegit
112
+ } else {
113
+ false
114
+ }
115
+
116
+ // Check suspicious manufacturer/model
117
+ val hasSuspiciousManufacturer = SUSPICIOUS_MANUFACTURERS.any {
118
+ val found = manufacturer.contains(it.lowercase())
119
+ if (found) {
120
+ BridgeUtils.logDebug(TAG, "Found suspicious manufacturer: '$it' in '$manufacturer'")
121
+ }
122
+ found
123
+ }
124
+ val hasSuspiciousModel = SUSPICIOUS_MODELS.any {
125
+ val found = model.contains(it.lowercase())
126
+ if (found) {
127
+ BridgeUtils.logDebug(TAG, "Found suspicious model: '$it' in '$model'")
128
+ }
129
+ found
130
+ }
131
+
132
+ // Final result: emulator if properties match AND not whitelisted, OR suspicious manufacturer/model
133
+ val result = (hasEmulatorProperty && !isLegitimateDevice) || hasSuspiciousManufacturer || hasSuspiciousModel
134
+ BridgeUtils.logDebug(TAG, "checkBuildProperties result: $result (emulatorProp=$hasEmulatorProperty, legitimateDevice=$isLegitimateDevice, suspiciousManufacturer=$hasSuspiciousManufacturer, suspiciousModel=$hasSuspiciousModel)")
135
+ return result
136
+ }
137
+
138
+ /**
139
+ * Check for missing hardware sensors (common in emulators)
140
+ */
141
+ private fun checkHardwareFeatures(context: Context): Boolean {
142
+ try {
143
+ val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager
144
+ ?: return true
145
+
146
+ // Real devices typically have these sensors
147
+ val hasAccelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null
148
+ val hasGyroscope = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null
149
+ val hasProximity = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY) != null
150
+ val hasLight = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT) != null
151
+
152
+ BridgeUtils.logDebug(TAG, "Sensor check - Accelerometer: $hasAccelerometer, Gyroscope: $hasGyroscope, Proximity: $hasProximity, Light: $hasLight")
153
+
154
+ // Count missing sensors
155
+ val missingSensors = listOf(hasAccelerometer, hasGyroscope, hasProximity, hasLight)
156
+ .count { !it }
157
+
158
+ // If 2 or more critical sensors missing, likely an emulator
159
+ val result = missingSensors >= 2
160
+ BridgeUtils.logDebug(TAG, "checkHardwareFeatures result: $result (missing $missingSensors/4 sensors)")
161
+ return result
162
+ } catch (e: Exception) {
163
+ BridgeUtils.logError(TAG, "Error checking hardware features", e)
164
+ return false
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Check Build.FINGERPRINT for emulator indicators (generic/unknown prefix, test-keys)
170
+ */
171
+ private fun checkFingerprint(): Boolean {
172
+ return try {
173
+ val fingerprint = Build.FINGERPRINT
174
+ val hasGeneric = fingerprint.startsWith("generic")
175
+ val hasUnknown = fingerprint.startsWith("unknown")
176
+ val hasTestKeys = fingerprint.lowercase().contains("test-keys")
177
+
178
+ val result = hasGeneric || hasUnknown || hasTestKeys
179
+ BridgeUtils.logDebug(TAG, "checkFingerprint result: $result (generic=$hasGeneric, unknown=$hasUnknown, testKeys=$hasTestKeys)")
180
+ BridgeUtils.logDebug(TAG, "Full FINGERPRINT: $fingerprint")
181
+ result
182
+ } catch (e: Exception) {
183
+ BridgeUtils.logError(TAG, "Error checking fingerprint", e)
184
+ false
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Get detailed emulator detection info for debugging
190
+ */
191
+ fun getEmulatorCheckDetails(context: Context): Map<String, Any> {
192
+ return mapOf(
193
+ "isEmulator" to isEmulator(context),
194
+ "buildCheck" to checkBuildProperties(),
195
+ "hardwareCheck" to checkHardwareFeatures(context),
196
+ "fingerprintCheck" to checkFingerprint(),
197
+ "fingerprint" to Build.FINGERPRINT,
198
+ "model" to Build.MODEL,
199
+ "manufacturer" to Build.MANUFACTURER,
200
+ "hardware" to Build.HARDWARE
201
+ )
202
+ }
203
+ }