feat: Implement CoreCaptureEngine and ScannerEngine for barcode scanning functionality

- Added CoreCaptureEngine to manage capture features and configurations.
- Introduced CaptureFeature enum to define available capture features.
- Created CaptureFeatureEngine interface for feature-specific implementations.
- Developed ScannerEngine to handle QR and barcode scanning using ML Kit.
- Implemented ScannerDelegate interface for handling scan results and errors.
- Added ScannerEngineActivity to provide a UI for scanning with Compose.
- Created UploadEngine class for handling uploads with presigned URLs.
- Established iCameraSDK for SDK initialization and telemetry reporting.
- Added TelemetryDetector for monitoring device and app telemetry.
- Defined ICameraSDKError enum for error handling within the SDK.
- Included example unit test for basic functionality verification.
- Updated settings.gradle to include the new SDK module.
This commit is contained in:
2025-12-16 23:26:25 +05:30
parent 2fc8e0f82c
commit 4461019a42
20 changed files with 1404 additions and 1 deletions

View File

@@ -124,4 +124,7 @@ dependencies {
// QR Code generation
implementation 'com.google.zxing:core:3.5.1'
// iCameraSDK integration
implementation(project(':sdk'))
}

View File

@@ -1,7 +1,7 @@
buildscript {
ext {
buildToolsVersion = "36.0.0"
minSdkVersion = 24
minSdkVersion = 26
compileSdkVersion = 36
targetSdkVersion = 36
ndkVersion = "27.1.12297006"

1
android/sdk/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,71 @@
plugins {
id("com.android.library")
kotlin("android")
id("org.jetbrains.kotlin.plugin.compose") version "2.0.21"
}
android {
namespace = "com.lynkedup.icamera.sdk"
compileSdk = 36
defaultConfig {
minSdk = 26
targetSdk = 36
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
// AndroidX
implementation("androidx.core:core-ktx:1.17.0")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("com.google.android.material:material:1.13.0")
implementation("androidx.activity:activity:1.11.0")
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.4")
// Camera dependencies
implementation("androidx.camera:camera-view:1.5.1")
implementation("androidx.camera:camera-lifecycle:1.5.1")
implementation("androidx.camera:camera-camera2:1.5.1")
// ML Kit Barcode Scanning
implementation("com.google.mlkit:barcode-scanning-common:17.0.0")
implementation("com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.1")
// Compose dependencies
implementation("androidx.activity:activity-compose:1.11.0")
implementation(platform("androidx.compose:compose-bom:2024.09.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3:1.4.0")
debugImplementation("androidx.compose.ui:ui-tooling")
// Testing
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
androidTestImplementation(platform("androidx.compose:compose-bom:2024.09.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}

View File

21
android/sdk/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,24 @@
package com.lynkedup.icamera.sdk
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.lynkedup.icamera.sdk.test", appContext.packageName)
}
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission
android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" />
</manifest>

View File

@@ -0,0 +1,59 @@
package com.lynkedup.icamera.sdk.CoreCaptureEngine
import android.content.Context
import android.widget.ImageView
import androidx.camera.view.PreviewView
import androidx.lifecycle.LifecycleOwner
import com.google.mlkit.vision.barcode.common.Barcode
import com.lynkedup.icamera.captureengine.scanner.ScannerEngine
import com.lynkedup.icamera.sdk.ScannerEngine.ScannerDelegate
import com.lynkedup.icamera.sdk.UploadEngine.UploadEngine
import java.lang.ref.WeakReference
import java.net.URL
public enum class CaptureFeature {
CAPTURE_3D,
QR_CODE,
BAR_CODE,
LENS
}
interface CaptureFeatureEngine{
fun start()
fun stop()
fun makePreview(imageView : PreviewView)
}
data class CoreCaptureConfiguration(
val enabledFeatures: List<CaptureFeature>,
val presignedUploadURL: URL
)
class CoreCaptureEngine(configuration: CoreCaptureConfiguration, context: Context, lifecycleOwner: LifecycleOwner) {
private val uploadEngine: UploadEngine = UploadEngine(configuration.presignedUploadURL)
private var featureEngines: MutableMap<CaptureFeature, CaptureFeatureEngine> = mutableMapOf()
var scannerDelegate: ScannerDelegate? = null
init {
for (feature in configuration.enabledFeatures) {
when (feature) {
// CaptureFeature.CAPTURE_3D -> featureEngines[CaptureFeature.CAPTURE_3D] = CaptureEngine()
CaptureFeature.QR_CODE -> featureEngines[CaptureFeature.QR_CODE] = ScannerEngine(
context = context,
lifecycleOwner = lifecycleOwner,
supportedFormats = listOf(Barcode.FORMAT_QR_CODE),
delegate = scannerDelegate)
CaptureFeature.BAR_CODE -> featureEngines[CaptureFeature.BAR_CODE] = ScannerEngine(
context = context,
lifecycleOwner=lifecycleOwner,
supportedFormats = listOf(Barcode.FORMAT_QR_CODE),
delegate = scannerDelegate)
// CaptureFeature.LENS -> featureEngines[CaptureFeature.LENS] = LensEngine()
else -> {}
}
}
}
}

View File

@@ -0,0 +1,6 @@
package com.lynkedup.icamera.sdk.ScannerEngine
interface ScannerDelegate {
fun onScanned(code: String, format: Int)
fun onScanFailed(error: Exception)
}

View File

@@ -0,0 +1,147 @@
package com.lynkedup.icamera.captureengine.scanner
import android.content.Context
import androidx.annotation.OptIn
import androidx.camera.core.CameraSelector
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import com.google.mlkit.vision.barcode.BarcodeScanner
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage
import com.lynkedup.icamera.sdk.CoreCaptureEngine.CaptureFeatureEngine
import com.lynkedup.icamera.sdk.ScannerEngine.ScannerDelegate
import java.lang.ref.WeakReference
import java.util.concurrent.Executors
class ScannerEngine(
private val context: Context,
private val lifecycleOwner: LifecycleOwner,
private val supportedFormats: List<Int> = listOf(Barcode.FORMAT_QR_CODE),
delegate: ScannerDelegate?
) : CaptureFeatureEngine {
private var previewView: PreviewView? = null
private val cameraExecutor = Executors.newSingleThreadExecutor()
private var cameraProvider: ProcessCameraProvider? = null
private var bound = false
private val delegate: WeakReference<ScannerDelegate?> = WeakReference(delegate)
override fun makePreview(imageView: PreviewView) {
if (imageView is PreviewView) {
previewView = imageView
} else {
// Or handle the error appropriately, maybe by logging or throwing an exception
// if PreviewView is strictly required.
previewView = PreviewView(imageView.context).also {
// You might need to add this new PreviewView to the layout manually.
// This part depends on how you want to handle a generic ImageView.
}
}
}
override fun start() {
startScanner()
}
private fun startScanner() {
val providerFuture = ProcessCameraProvider.getInstance(context)
providerFuture.addListener({
try {
cameraProvider = providerFuture.get()
bindCameraUseCases()
} catch (e: Exception) {
delegate.get()?.onScanFailed(e)
}
}, ContextCompat.getMainExecutor(context))
}
private fun bindCameraUseCases() {
val provider = cameraProvider ?: run {
delegate.get()?.onScanFailed(IllegalStateException("CameraProvider not initialized"))
return
}
// Unbind previous use cases
provider.unbindAll()
val preview = Preview.Builder().build().also { p ->
previewView?.let { p.surfaceProvider = it.surfaceProvider }
}
val options = BarcodeScannerOptions.Builder().apply {
if (supportedFormats.isEmpty()) {
setBarcodeFormats(Barcode.FORMAT_QR_CODE)
} else {
val first = supportedFormats.first()
val rest = if (supportedFormats.size > 1) supportedFormats.drop(1).toIntArray() else intArrayOf()
setBarcodeFormats(first, *rest)
}
}.build()
val scanner = BarcodeScanning.getClient(options)
val analysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
analysis.setAnalyzer(cameraExecutor) { imageProxy: ImageProxy ->
processImageProxy(scanner, imageProxy)
}
try {
provider.bindToLifecycle(lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis)
bound = true
} catch (ex: Exception) {
delegate.get()?.onScanFailed(ex)
}
}
@OptIn(ExperimentalGetImage::class)
private fun processImageProxy(scanner: BarcodeScanner, imageProxy: ImageProxy) {
val mediaImage = imageProxy.image
if (mediaImage == null) {
imageProxy.close()
return
}
val rotationDegrees = imageProxy.imageInfo.rotationDegrees
val inputImage = InputImage.fromMediaImage(mediaImage, rotationDegrees)
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
if (barcodes.isNotEmpty()) {
val barcode = barcodes.first()
val raw = barcode.rawValue
val format = barcode.format
if (!raw.isNullOrEmpty()) {
delegate.get()?.onScanned(raw, format)
}
}
}
.addOnFailureListener { e ->
delegate.get()?.onScanFailed(Exception(e))
}
.addOnCompleteListener {
imageProxy.close()
}
}
override fun stop() {
try {
cameraProvider?.unbindAll()
} catch (_: Exception) {
}
bound = false
// Do not shut down executor immediately if you plan to restart quickly; otherwise:
if (!cameraExecutor.isShutdown) {
cameraExecutor.shutdown()
}
}
}

View File

@@ -0,0 +1,72 @@
package com.lynkedup.icamera.sdk.ScannerEngine
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.camera.view.PreviewView
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.LifecycleOwner
import com.lynkedup.icamera.captureengine.scanner.ScannerEngine
class ScannerEngineActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!com.lynkedup.icamera.sdk.iCameraSDK.isInitialized) {
throw Exception("iCameraSDK is not initialized. Call initialize(...) first.")
}
setContent {
MaterialTheme {
Surface {
ScannerEngineScreen()
}
}
}
}
}
@Composable
fun ScannerEngineScreen() {
val context = LocalContext.current
val lifecycleOwner = context as LifecycleOwner
val previewView = remember { PreviewView(context) }
val scannerEngine = remember {
ScannerEngine(
context = context,
lifecycleOwner = lifecycleOwner,
supportedFormats = listOf(), // Add supported formats as needed
delegate = object : ScannerDelegate {
override fun onScanned(code: String, format: Int) {
Toast.makeText(context, "Scanned: $code", Toast.LENGTH_SHORT).show()
}
override fun onScanFailed(error: Exception) {
Toast.makeText(context, "Scan failed: ${'$'}{error.message}", Toast.LENGTH_SHORT).show()
}
}
).apply {
makePreview(previewView)
}
}
DisposableEffect(Unit) {
scannerEngine.start()
onDispose {
scannerEngine.stop()
}
}
AndroidView(
factory = { previewView },
modifier = Modifier.fillMaxSize()
)
}

View File

@@ -0,0 +1,6 @@
package com.lynkedup.icamera.sdk.UploadEngine
import java.net.URL
class UploadEngine(uploadURL: URL) {
}

View File

@@ -0,0 +1,89 @@
package com.lynkedup.icamera.sdk
import android.content.Context
import android.util.Log
import com.lynkedup.icamera.sdk.CoreCaptureEngine.CaptureFeature
import com.lynkedup.icamera.sdk.idp_kit.TelemeteryReporter
import com.lynkedup.icamera.sdk.idp_kit.TelemetryDetector
object iCameraSDK {
@Volatile
private var config: SDKConfig? = null
@Volatile
private var telemetry: TelemetryDetector? = null
private val telemetryListener = object : TelemeteryReporter {
override fun onReport(json: String) {
Log.d("iCameraSDK.Telemetry", json)
}
}
val isInitialized: Boolean
get() = config != null
/**
* Initialize the SDK. Safe to call from any thread. Uses application context.
* Throws IllegalArgumentException on invalid inputs.
*/
fun initialize(context: Context, presignedUrl: String, features: List<CaptureFeature>) {
require(presignedUrl.isNotBlank()) { "presignedUrl must not be empty" }
require(features.isNotEmpty()) { "features must contain at least one CaptureFeature" }
val appCtx = context.applicationContext
val cfg = SDKConfig(appContext = appCtx, presignedUrl = presignedUrl.trim(), features = features.toSet())
config = cfg
// start telemetry after config is set
startTelemetryIfNeeded(appCtx)
}
fun initialize(context: Context, presignedUrl: String, vararg features: CaptureFeature) {
initialize(context, presignedUrl, features.toList())
}
fun getConfig(): SDKConfig {
return config ?: throw IllegalStateException("iCameraSDK is not initialized. Call initialize(...) first.")
}
/**
* Clears SDK state. Use carefully (for tests or app shutdown).
*/
fun shutdown() {
stopTelemetry()
config = null
}
private fun startTelemetryIfNeeded(appCtx: Context) {
if (telemetry != null) return
try {
val td = TelemetryDetector(appCtx, telemetryListener)
td.start()
telemetry = td
Log.d("iCameraSDK", "Telemetry started")
} catch (e: Exception) {
Log.w("iCameraSDK", "Failed to start telemetry: ${e.message}")
}
}
private fun stopTelemetry() {
try {
telemetry?.stop()
telemetry = null
Log.d("iCameraSDK", "Telemetry stopped")
} catch (e: Exception) {
Log.w("iCameraSDK", "Failed to stop telemetry: ${e.message}")
}
}
data class SDKConfig(
val appContext: Context,
val presignedUrl: String,
val features: Set<CaptureFeature>
)
}

View File

@@ -0,0 +1,6 @@
package com.lynkedup.icamera.sdk.idp_kit
interface TelemeteryReporter {
fun onReport(json: String)
}

View File

@@ -0,0 +1,833 @@
package com.lynkedup.icamera.sdk.idp_kit
import android.Manifest
import android.app.ActivityManager
import android.app.ActivityManager.MemoryInfo
import android.app.AppOpsManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.WifiManager
import android.os.BatteryManager
import android.os.Binder
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.StatFs
import android.os.SystemClock
import android.provider.Settings
import android.telephony.PhoneStateListener
import android.telephony.TelephonyManager
import android.text.format.Formatter
import android.util.Log
import androidx.core.content.ContextCompat
import org.json.JSONArray
import org.json.JSONObject
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
/**
*{
* "trigger": "wifi_change",
* "timestamp": 1764607091305,
* "uptime_millis": 560764,
* "sdk_int": 35,
* "package_name": "com.lynkedup.icamera.app",
* "app": {
* "version_name": "1.0",
* "version_code": 1,
* "first_install_time": 1764443073758,
* "last_update_time": 1764443073758,
* "requested_permissions": [
* {
* "name": "android.permission.INTERNET",
* "granted": true
* },
* {
* "name": "android.permission.ACCESS_NETWORK_STATE",
* "granted": true
* },
* {
* "name": "android.permission.READ_PRIVILEGED_PHONE_STATE",
* "granted": false
* },
* {
* "name": "android.permission.READ_PHONE_STATE",
* "granted": false
* },
* {
* "name": "android.permission.ACCESS_WIFI_STATE",
* "granted": true
* },
* {
* "name": "android.permission.WAKE_LOCK",
* "granted": true
* },
* {
* "name": "android.permission.RECEIVE_BOOT_COMPLETED",
* "granted": true
* },
* {
* "name": "android.permission.ACCESS_COARSE_LOCATION",
* "granted": false
* },
* {
* "name": "com.lynkedup.icamera.app.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION",
* "granted": true
* }
* ]
* },
* "permissions": {
* "android.permission.ACCESS_NETWORK_STATE": true,
* "android.permission.INTERNET": true,
* "android.permission.WAKE_LOCK": true,
* "android.permission.QUERY_ALL_PACKAGES": false,
* "android.permission.READ_PHONE_STATE": false,
* "android.permission.ACTIVITY_RECOGNITION": false,
* "android.permission.RECEIVE_BOOT_COMPLETED": true,
* "android.permission.ACCESS_WIFI_STATE": true,
* "android.permission.PACKAGE_USAGE_STATS": false,
* "android.permission.FOREGROUND_SERVICE_DATA_SYNC": false
* },
* "usage_stats_allowed": false,
* "network": {
* "isConnected": true,
* "type": "WIFI",
* "type_code": null,
* "down_kbps": 30000,
* "up_kbps": 12000,
* "subtype_name": null,
* "subtype": null
* },
* "wifi": {
* "ssid": "<unknown ssid>",
* "bssid": "02:00:00:00:00:00",
* "rssi": -50,
* "link_speed_mbps": 11,
* "local_ip": "10.0.2.16"
* },
* "wifi_ssid": "<unknown ssid>",
* "device_id": null,
* "battery": {
* "level_percent": 100,
* "status": "not_charging"
* },
* "memory": {
* "avail_mem": 888184832,
* "total_mem": 2070986752,
* "low_memory": false
* },
* "storage": {
* "total_bytes": 6228115456,
* "free_bytes": 996352000
* },
* "developer_options_enabled": false,
* "is_rooted": false,
* "root_checks": [
*
* ],
* "is_emulator": true,
* "device_type": "EMULATOR",
* "emulator_checks": [
* "product_indicates_emulator",
* "hardware_indicates_emulator"
* ]
* }
*
*
* */
class TelemetryDetector(private val context: Context, private val listener: TelemeteryReporter) {
private val appContext = context.applicationContext
private val connectivity =
appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private val wifiManager = appContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
private val telephony =
appContext.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
private var networkCallback: ConnectivityManager.NetworkCallback? = null
private var wifiReceiver: BroadcastReceiver? = null
private var packageReceiver: BroadcastReceiver? = null
private var phoneListener: PhoneStateListener? = null
private var running = false
// periodic reporting
private val reportIntervalMs = 15000L
private val reportHandler = Handler(Looper.getMainLooper())
private var reportRunnable: Runnable? = null
fun start() {
if (running) return
running = true
registerNetworkCallback()
registerWifiReceiver()
registerPackageReceiver()
registerPhoneListener()
// emit initial snapshot
emitReport("initial")
// schedule periodic reports
schedulePeriodicReports()
}
fun stop() {
if (!running) return
running = false
unregisterNetworkCallback()
unregisterWifiReceiver()
unregisterPackageReceiver()
unregisterPhoneListener()
cancelPeriodicReports()
}
private fun schedulePeriodicReports() {
// ensure previous runnable removed
cancelPeriodicReports()
val r = object : Runnable {
override fun run() {
try {
if (!running) return
emitReport("periodic")
} finally {
// re-post only if still running
if (running) {
reportHandler.postDelayed(this, reportIntervalMs)
}
}
}
}
reportRunnable = r
reportHandler.postDelayed(r, reportIntervalMs)
}
private fun cancelPeriodicReports() {
reportRunnable?.let {
reportHandler.removeCallbacks(it)
}
reportRunnable = null
}
private fun registerNetworkCallback() {
try {
val cb = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
emitReport("network_available")
}
override fun onLost(network: Network) {
emitReport("network_lost")
}
}
val req = NetworkRequest.Builder().build()
connectivity.registerNetworkCallback(req, cb)
networkCallback = cb
} catch (e: Exception) {
Log.w("TelemetryDetector", "network callback failed: ${e.message}")
}
}
private fun unregisterNetworkCallback() {
try {
networkCallback?.let { connectivity.unregisterNetworkCallback(it) }
} catch (_: Exception) {
}
networkCallback = null
}
private fun registerWifiReceiver() {
try {
val rx = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action
if (action == WifiManager.NETWORK_STATE_CHANGED_ACTION || action == WifiManager.WIFI_STATE_CHANGED_ACTION) {
emitReport("wifi_change")
}
}
}
val filter = IntentFilter().apply {
addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION)
addAction(WifiManager.WIFI_STATE_CHANGED_ACTION)
}
appContext.registerReceiver(rx, filter)
wifiReceiver = rx
} catch (e: Exception) {
Log.w("TelemetryDetector", "wifi receiver failed: ${e.message}")
}
}
private fun unregisterWifiReceiver() {
try {
wifiReceiver?.let { appContext.unregisterReceiver(it) }
} catch (_: Exception) {
}
wifiReceiver = null
}
private fun registerPackageReceiver() {
try {
val rx = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action
if (action == Intent.ACTION_PACKAGE_ADDED || action == Intent.ACTION_PACKAGE_REMOVED || action == Intent.ACTION_PACKAGE_CHANGED) {
emitReport("package_change")
}
}
}
val filter = IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addAction(Intent.ACTION_PACKAGE_CHANGED)
addDataScheme("package")
}
appContext.registerReceiver(rx, filter)
packageReceiver = rx
} catch (e: Exception) {
Log.w("TelemetryDetector", "package receiver failed: ${e.message}")
}
}
private fun unregisterPackageReceiver() {
try {
packageReceiver?.let { appContext.unregisterReceiver(it) }
} catch (_: Exception) {
}
packageReceiver = null
}
private fun registerPhoneListener() {
try {
val pl = object : PhoneStateListener() {
override fun onCallStateChanged(state: Int, phoneNumber: String?) {
emitReport("phone_state_change")
}
}
telephony.listen(pl, PhoneStateListener.LISTEN_CALL_STATE)
phoneListener = pl
} catch (e: Exception) {
Log.w("TelemetryDetector", "phone listener failed: ${e.message}")
}
}
private fun unregisterPhoneListener() {
try {
phoneListener?.let { telephony.listen(it, PhoneStateListener.LISTEN_NONE) }
} catch (_: Exception) {
}
phoneListener = null
}
private fun emitReport(trigger: String) {
try {
val pretty = buildReport(trigger)
Log.d("TelemetryDetector", pretty) // pretty printed JSON
listener.onReport(pretty)
} catch (e: Exception) {
Log.w("TelemetryDetector", "emit failed: ${e.message}")
}
}
private fun buildReport(trigger: String): String {
val report = JSONObject()
report.put("trigger", trigger)
report.put("timestamp", System.currentTimeMillis())
report.put("uptime_millis", SystemClock.elapsedRealtime())
report.put("sdk_int", Build.VERSION.SDK_INT)
report.put("package_name", appContext.packageName)
// app / package info
try {
val pm = appContext.packageManager
@Suppress("DEPRECATION")
val pkgInfo: PackageInfo? = try {
pm.getPackageInfo(appContext.packageName, PackageManager.GET_PERMISSIONS)
} catch (e: Exception) {
try {
pm.getPackageInfo(appContext.packageName, 0)
} catch (_: Exception) {
null
}
}
pkgInfo?.let {
val appJson = JSONObject()
val versionName = try {
it.versionName
} catch (_: Exception) {
null
}
val versionCode = try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) it.longVersionCode else it.versionCode.toLong()
} catch (_: Exception) {
null
}
appJson.put("version_name", versionName ?: JSONObject.NULL)
appJson.put("version_code", versionCode ?: JSONObject.NULL)
appJson.put("first_install_time", it.firstInstallTime)
appJson.put("last_update_time", it.lastUpdateTime)
// requested permissions + grant state
try {
val permsArr = JSONArray()
val requested = it.requestedPermissions
if (requested != null) {
for (p in requested) {
val pJson = JSONObject()
pJson.put("name", p)
val granted = try {
ContextCompat.checkSelfPermission(
appContext,
p
) == PackageManager.PERMISSION_GRANTED
} catch (_: Exception) {
false
}
pJson.put("granted", granted)
permsArr.put(pJson)
}
}
appJson.put("requested_permissions", permsArr)
} catch (_: Exception) {
// ignore
}
report.put("app", appJson)
}
} catch (e: Exception) {
report.put("app_info_error", e.javaClass.simpleName)
}
// permissions of interest (explicit list)
try {
val permissionList = listOf(
Manifest.permission.ACCESS_NETWORK_STATE,
Manifest.permission.INTERNET,
Manifest.permission.WAKE_LOCK,
"android.permission.QUERY_ALL_PACKAGES",
Manifest.permission.READ_PHONE_STATE,
"android.permission.ACTIVITY_RECOGNITION",
Manifest.permission.RECEIVE_BOOT_COMPLETED,
Manifest.permission.ACCESS_WIFI_STATE,
"android.permission.PACKAGE_USAGE_STATS",
"android.permission.FOREGROUND_SERVICE_DATA_SYNC"
)
val permsJson = JSONObject()
for (perm in permissionList) {
val granted = try {
ContextCompat.checkSelfPermission(
appContext,
perm
) == PackageManager.PERMISSION_GRANTED
} catch (_: Exception) {
false
}
permsJson.put(perm, granted)
}
report.put("permissions", permsJson)
} catch (e: Exception) {
report.put("permissions_error", e.javaClass.simpleName)
}
// usage access check
try {
val appOps = appContext.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
val hasUsageAccess = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
appOps.checkOpNoThrow(
AppOpsManager.OPSTR_GET_USAGE_STATS,
Binder.getCallingUid(),
appContext.packageName
) == AppOpsManager.MODE_ALLOWED
} else {
false
}
report.put("usage_stats_allowed", hasUsageAccess)
} catch (e: Exception) {
report.put("usage_stats_error", e.javaClass.simpleName)
}
// network basic
try {
val networkJson = JSONObject()
val activeNetwork = connectivity.activeNetwork
if (activeNetwork != null) {
val caps = try {
connectivity.getNetworkCapabilities(activeNetwork)
} catch (_: kotlin.Exception) {
null
}
val isConnected = caps != null && (
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ||
caps.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH)
)
networkJson.put("isConnected", isConnected)
val type = when {
caps == null -> JSONObject.NULL
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> "WIFI"
caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> "CELLULAR"
caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> "ETHERNET"
caps.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> "BLUETOOTH"
else -> "OTHER"
}
networkJson.put("type", type)
// no direct type_code with new API; keep numeric fields null for compatibility
networkJson.put("type_code", JSONObject.NULL)
// bandwidth (kbps) if available
networkJson.put("down_kbps", caps?.linkDownstreamBandwidthKbps ?: JSONObject.NULL)
networkJson.put("up_kbps", caps?.linkUpstreamBandwidthKbps ?: JSONObject.NULL)
// attempt to include telephony subtype info when on cellular (numeric values)
val subtypeName: Any = try {
if (caps?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true) {
// numeric network type; caller can map if needed
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) telephony.dataNetworkType else telephony.networkType
} else JSONObject.NULL
} catch (_: kotlin.Exception) {
JSONObject.NULL
}
networkJson.put("subtype_name", subtypeName)
networkJson.put("subtype", JSONObject.NULL) // keep for compatibility
} else {
networkJson.put("isConnected", false)
networkJson.put("type", JSONObject.NULL)
networkJson.put("type_code", JSONObject.NULL)
networkJson.put("subtype_name", JSONObject.NULL)
networkJson.put("subtype", JSONObject.NULL)
networkJson.put("down_kbps", JSONObject.NULL)
networkJson.put("up_kbps", JSONObject.NULL)
}
report.put("network", networkJson)
} catch (e: Exception) {
report.put("network_error", e.javaClass.simpleName)
}
// wifi details
try {
val wifiJson = JSONObject()
@Suppress("DEPRECATION")
val info = wifiManager.connectionInfo
if (info != null) {
val ssid = try {
info.ssid
} catch (_: Exception) {
null
}
val bssid = try {
info.bssid
} catch (_: Exception) {
null
}
val rssi = try {
info.rssi
} catch (_: Exception) {
null
}
val linkSpeed = try {
info.linkSpeed
} catch (_: Exception) {
null
}
val ipInt = try {
info.ipAddress
} catch (_: Exception) {
0
}
val ip = try {
if (ipInt != 0) Formatter.formatIpAddress(ipInt) else JSONObject.NULL
} catch (_: Exception) {
JSONObject.NULL
}
wifiJson.put("ssid", ssid ?: JSONObject.NULL)
wifiJson.put("bssid", bssid ?: JSONObject.NULL)
wifiJson.put("rssi", rssi ?: JSONObject.NULL)
wifiJson.put("link_speed_mbps", linkSpeed ?: JSONObject.NULL)
wifiJson.put("local_ip", ip)
} else {
wifiJson.put("connected", false)
}
report.put("wifi", wifiJson)
report.put("wifi_ssid", (info?.ssid ?: JSONObject.NULL))
} catch (e: Exception) {
report.put("wifi_error", e.javaClass.simpleName)
}
// device id only if permission granted
try {
var deviceId: Any = JSONObject.NULL
if (ContextCompat.checkSelfPermission(
appContext,
Manifest.permission.READ_PHONE_STATE
) == PackageManager.PERMISSION_GRANTED
) {
deviceId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
telephony.imei ?: JSONObject.NULL
} catch (_: Throwable) {
JSONObject.NULL
}
} else {
@Suppress("DEPRECATION")
telephony.deviceId ?: JSONObject.NULL
}
}
report.put("device_id", deviceId)
} catch (e: Exception) {
report.put("device_id_error", e.javaClass.simpleName)
}
// battery info
try {
val battIntent =
appContext.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
val battJson = JSONObject()
if (battIntent != null) {
val level = battIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val scale = battIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
val pct = if (level >= 0 && scale > 0) (level * 100) / scale else JSONObject.NULL
val status = battIntent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
val statusStr = when (status) {
BatteryManager.BATTERY_STATUS_CHARGING -> "charging"
BatteryManager.BATTERY_STATUS_FULL -> "full"
BatteryManager.BATTERY_STATUS_DISCHARGING -> "discharging"
BatteryManager.BATTERY_STATUS_NOT_CHARGING -> "not_charging"
BatteryManager.BATTERY_STATUS_UNKNOWN -> "unknown"
else -> "unknown"
}
battJson.put("level_percent", pct)
battJson.put("status", statusStr)
}
report.put("battery", battJson)
} catch (e: Exception) {
report.put("battery_error", e.javaClass.simpleName)
}
// memory info
try {
val am = appContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val mi = MemoryInfo()
am.getMemoryInfo(mi)
val memJson = JSONObject()
memJson.put("avail_mem", mi.availMem)
memJson.put(
"total_mem",
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) mi.totalMem else JSONObject.NULL
)
memJson.put("low_memory", mi.lowMemory)
report.put("memory", memJson)
} catch (e: Exception) {
report.put("memory_error", e.javaClass.simpleName)
}
// storage info (app data dir)
try {
val dataDir = appContext.filesDir
val st = StatFs(dataDir.absolutePath)
val blockSize = st.blockSizeLong
val total = st.blockCountLong * blockSize
val free = st.availableBlocksLong * blockSize
val storageJson = JSONObject()
storageJson.put("total_bytes", total)
storageJson.put("free_bytes", free)
report.put("storage", storageJson)
} catch (e: Exception) {
report.put("storage_error", e.javaClass.simpleName)
}
// developer options enabled
try {
val devEnabled = isDeveloperOptionsEnabled()
report.put("developer_options_enabled", devEnabled)
} catch (e: Exception) {
report.put("developer_options_error", e.javaClass.simpleName)
}
// root detection
try {
val rootChecks = JSONArray()
val isRooted = isDeviceRooted(rootChecks)
report.put("is_rooted", isRooted)
report.put("root_checks", rootChecks)
} catch (e: Exception) {
report.put("root_detection_error", e.javaClass.simpleName)
}
try {
val emuChecks = JSONArray()
val isEmu = detectEmulator(emuChecks)
report.put("is_emulator", isEmu)
report.put("device_type", if (isEmu) "EMULATOR" else "PHYSICAL")
if (emuChecks.length() > 0) report.put("emulator_checks", emuChecks)
} catch (e: Exception) {
report.put("emulator_detection_error", e.javaClass.simpleName)
}
// final pretty string (indent = 2)
return try {
report.toString(2)
} catch (e: Exception) {
// fallback to compact
report.toString()
}
}
private fun isDeveloperOptionsEnabled(): Boolean {
return try {
val resolver = appContext.contentResolver
val enabled = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
Settings.Global.getInt(resolver, Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0)
} else {
@Suppress("DEPRECATION")
Settings.Secure.getInt(resolver, Settings.Secure.DEVELOPMENT_SETTINGS_ENABLED, 0)
}
enabled == 1
} catch (_: Exception) {
false
}
}
private fun isDeviceRooted(outReasons: JSONArray): Boolean {
try {
// 1) Build tags
val tags = Build.TAGS
if (!tags.isNullOrEmpty() && (tags.contains("test-keys") || tags.contains("dev-keys"))) {
outReasons.put("build_tags_contains_test_or_dev_keys")
}
// 2) Common su locations
val suPaths = arrayOf(
"/system/app/Superuser.apk",
"/sbin/su",
"/system/bin/su",
"/system/xbin/su",
"/data/local/xbin/su",
"/data/local/bin/su",
"/system/sd/xbin/su",
"/system/bin/failsafe/su",
"/data/local/su"
)
for (p in suPaths) {
try {
if (File(p).exists()) {
outReasons.put("su_binary_found:$p")
}
} catch (_: Exception) { /* ignore */
}
}
// 3) Presence of su in PATH via `which su`
try {
val proc = Runtime.getRuntime().exec(arrayOf("/system/bin/sh", "-c", "which su"))
BufferedReader(InputStreamReader(proc.inputStream)).use { br ->
val line = br.readLine()
if (!line.isNullOrEmpty()) {
outReasons.put("which_su_found:$line")
}
}
} catch (_: Exception) {
// ignore failures
}
// 4) Superuser.apk in package manager (loosely)
try {
val pm = appContext.packageManager
val packages = pm.getInstalledPackages(0)
for (pkg in packages) {
val name = pkg.packageName
if (name.equals("com.noshufou.android.su", ignoreCase = true) ||
name.equals("eu.chainfire.supersu", ignoreCase = true) ||
name.equals("com.koushikdutta.superuser", ignoreCase = true) ||
name.contains("super", ignoreCase = true) && name.contains(
"user",
ignoreCase = true
)
) {
outReasons.put("suspicious_superuser_package:$name")
break
}
}
} catch (_: Exception) {
// ignore
}
return outReasons.length() > 0
} catch (_: Exception) {
return false
}
}
private fun detectEmulator(outReasons: JSONArray): Boolean {
try {
val fingerprint = Build.FINGERPRINT ?: ""
val model = Build.MODEL ?: ""
val manufacturer = Build.MANUFACTURER ?: ""
val brand = Build.BRAND ?: ""
val device = Build.DEVICE ?: ""
val product = Build.PRODUCT ?: ""
val hardware = Build.HARDWARE ?: ""
val board = Build.BOARD ?: ""
val reasons = mutableListOf<String>()
if (fingerprint.startsWith("generic") || fingerprint.startsWith("unknown")) {
reasons.add("fingerprint_generic_or_unknown")
}
if (model.contains("Emulator", ignoreCase = true) ||
model.contains("Android SDK built for", ignoreCase = true) ||
model.contains("google_sdk", ignoreCase = true)
) {
reasons.add("model_indicates_emulator")
}
if (manufacturer.contains("Genymotion", ignoreCase = true)) {
reasons.add("manufacturer_genymotion")
}
if (brand.startsWith("generic") && device.startsWith("generic")) {
reasons.add("brand_and_device_generic")
}
if (product.contains("sdk", ignoreCase = true) ||
product.contains("vbox86p", ignoreCase = true) ||
product.contains("emulator", ignoreCase = true)
) {
reasons.add("product_indicates_emulator")
}
if (hardware.contains("goldfish", ignoreCase = true) ||
hardware.contains("ranchu", ignoreCase = true) ||
hardware.contains("qemu", ignoreCase = true)
) {
reasons.add("hardware_indicates_emulator")
}
if (board.equals("unknown", ignoreCase = true)) {
reasons.add("board_unknown")
}
// common emulator files / sockets
try {
if (File("/dev/socket/qemu_pipe").exists()) reasons.add("qemu_pipe_present")
if (File("/system/lib/libc_malloc_debug_qemu.so").exists()) reasons.add("qemu_so_present")
if (File("/system/bin/qemu-props").exists()) reasons.add("qemu_props_present")
} catch (_: kotlin.Exception) { /* ignore filesystem access errors */
}
if (reasons.isNotEmpty()) {
for (r in reasons) outReasons.put(r)
return true
}
return false
} catch (_: kotlin.Exception) {
return false
}
}
}

View File

@@ -0,0 +1,4 @@
package com.lynkedup.icamera.sdk.utils
class iCameraSdkConfiguration {
}

View File

@@ -0,0 +1,27 @@
package com.lynkedup.icamera.sdk.utils
/**
* Represents a set of possible errors that can occur within the iCameraSDK.
*/
public enum class ICameraSDKError {
/**
* The user has denied access to the camera.
*/
CAMERA_ACCESS_DENIED,
/**
* The configuration provided to the SDK is invalid.
*/
INVALID_CONFIGURATION,
/**
* The file upload process failed.
*/
UPLOAD_FAILED,
/**
* An unknown or unspecified error occurred.
*/
UNKNOWN
}

View File

@@ -0,0 +1,17 @@
package com.lynkedup.icamera.sdk
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@@ -3,4 +3,5 @@ plugins { id("com.facebook.react.settings") }
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
rootProject.name = 'LynkedUpPro'
include ':app'
include ':sdk'
includeBuild('../node_modules/@react-native/gradle-plugin')