ilabs-flir 1.0.3 → 1.0.4

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 (24) hide show
  1. package/android/Flir/libs/flir-stubs.jar +0 -0
  2. package/android/Flir/src/main/java/com/flir/thermalsdk/ErrorCode.java +13 -0
  3. package/android/Flir/src/main/java/com/flir/thermalsdk/ThermalSdkAndroid.java +16 -0
  4. package/android/Flir/src/main/java/com/flir/thermalsdk/androidsdk/image/BitmapAndroid.java +20 -0
  5. package/android/Flir/src/main/java/com/flir/thermalsdk/live/Camera.java +26 -0
  6. package/android/Flir/src/main/java/com/flir/thermalsdk/live/ConnectParameters.java +16 -0
  7. package/android/Flir/src/main/java/com/flir/thermalsdk/live/RemoteControl.java +16 -0
  8. package/android/Flir/src/main/java/com/flir/thermalsdk/live/discovery/DiscoveryEventListener.java +14 -0
  9. package/android/Flir/src/main/java/com/flir/thermalsdk/live/discovery/DiscoveryFactory.java +33 -0
  10. package/android/Flir/src/main/java/com/flir/thermalsdk/live/streaming/Stream.java +8 -0
  11. package/android/Flir/src/main/java/com/flir/thermalsdk/live/streaming/ThermalStreamer.java +28 -0
  12. package/android/Flir/src/main/java/com/flir/thermalsdk/live/streaming/VisualStreamer.java +18 -0
  13. package/android/Flir/src/main/java/flir/android/FlirCommands.java +40 -15
  14. package/android/Flir/src/main/java/flir/android/FlirDownloadManager.kt +6 -5
  15. package/android/Flir/src/main/java/flir/android/FlirManager.kt +265 -42
  16. package/android/Flir/src/main/java/flir/android/FlirSDKLoader.kt +242 -194
  17. package/android/Flir/src/main/java/flir/android/FlirSdkManager.java +1376 -755
  18. package/package.json +1 -1
  19. package/sdk-manifest.json +14 -7
  20. package/android/Flir/src/main/java/flir/android/CameraHandler.java +0 -224
  21. package/android/Flir/src/main/java/flir/android/FlirConnectionManager.java +0 -354
  22. package/android/Flir/src/main/java/flir/android/FlirController.kt +0 -11
  23. package/android/Flir/src/main/java/flir/android/FlirDiscoveryManager.java +0 -236
  24. package/android/Flir/src/main/java/flir/android/FrameDataHolder.java +0 -14
@@ -1,195 +1,243 @@
1
- package flir.android
2
-
3
- import android.content.Context
4
- import com.google.android.play.core.splitinstall.*
5
- import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus
6
- import kotlinx.coroutines.*
7
- import org.json.JSONObject
8
- import java.io.File
9
- import java.io.FileOutputStream
10
- import java.net.URL
11
- import java.security.MessageDigest
12
- import java.util.zip.ZipInputStream
13
-
14
- object FlirSDKLoader {
15
-
16
- private const val FEATURE_MODULE = "flir_sdk"
17
- private var splitInstallManager: SplitInstallManager? = null
18
-
19
- fun init(context: Context) {
20
- splitInstallManager = SplitInstallManagerFactory.create(context)
21
- }
22
-
23
- private fun getSDKDirectory(context: Context) = File(context.filesDir, "FlirSDK")
24
-
25
- fun isSDKAvailable(context: Context): Boolean {
26
- // Check Play Feature module
27
- splitInstallManager?.installedModules?.let {
28
- if (FEATURE_MODULE in it) return true
29
- }
30
- // Check direct download - look for either file from the manifest
31
- val sdkDir = getSDKDirectory(context)
32
- return File(sdkDir, "thermalsdk-release.aar").exists() ||
33
- File(sdkDir, "androidsdk-release.aar").exists() ||
34
- File(sdkDir, "thermalsdk.aar").exists()
35
- }
36
-
37
- fun getDownloadSize(context: Context): Long {
38
- return loadManifest(context)?.android?.directDownload?.sizeBytes ?: 52_428_800L
39
- }
40
-
41
- fun downloadViaPlayStore(
42
- onProgress: (Float) -> Unit,
43
- onComplete: () -> Unit,
44
- onError: (String) -> Unit
45
- ) {
46
- val manager = splitInstallManager ?: run {
47
- onError("SplitInstallManager not initialized")
48
- return
49
- }
50
-
51
- val request = SplitInstallRequest.newBuilder()
52
- .addModule(FEATURE_MODULE)
53
- .build()
54
-
55
- manager.registerListener { state ->
56
- when (state.status()) {
57
- SplitInstallSessionStatus.DOWNLOADING -> {
58
- val progress = state.bytesDownloaded().toFloat() / state.totalBytesToDownload()
59
- onProgress(progress)
60
- }
61
- SplitInstallSessionStatus.INSTALLED -> onComplete()
62
- SplitInstallSessionStatus.FAILED -> onError("Install failed: ${state.errorCode()}")
63
- else -> {}
64
- }
65
- }
66
-
67
- manager.startInstall(request)
68
- }
69
-
70
- suspend fun downloadDirect(
71
- context: Context,
72
- onProgress: (downloaded: Long, total: Long) -> Unit
73
- ): Result<Unit> = withContext(Dispatchers.IO) {
74
- try {
75
- val manifest = loadManifest(context) ?: return@withContext Result.failure(
76
- Exception("Failed to load manifest")
77
- )
78
-
79
- val downloadUrl = manifest.android.directDownload.downloadUrl
80
- val expectedHash = manifest.android.directDownload.sha256
81
- val totalSize = manifest.android.directDownload.sizeBytes
82
-
83
- val sdkDir = getSDKDirectory(context).apply { mkdirs() }
84
- val zipFile = File(context.cacheDir, "flir-sdk.zip")
85
-
86
- // Download
87
- URL(downloadUrl).openStream().use { input ->
88
- FileOutputStream(zipFile).use { output ->
89
- val buffer = ByteArray(8192)
90
- var totalRead = 0L
91
- var bytesRead: Int
92
-
93
- while (input.read(buffer).also { bytesRead = it } != -1) {
94
- output.write(buffer, 0, bytesRead)
95
- totalRead += bytesRead
96
- withContext(Dispatchers.Main) {
97
- onProgress(totalRead, totalSize)
98
- }
99
- }
100
- }
101
- }
102
-
103
- // Verify checksum
104
- val actualHash = sha256(zipFile)
105
- if (actualHash != expectedHash) {
106
- zipFile.delete()
107
- return@withContext Result.failure(SecurityException("Checksum mismatch"))
108
- }
109
-
110
- // Extract
111
- unzip(zipFile, sdkDir)
112
- zipFile.delete()
113
-
114
- Result.success(Unit)
115
- } catch (e: Exception) {
116
- Result.failure(e)
117
- }
118
- }
119
-
120
- fun deleteSDK(context: Context): Boolean {
121
- splitInstallManager?.deferredUninstall(listOf(FEATURE_MODULE))
122
- return getSDKDirectory(context).deleteRecursively()
123
- }
124
-
125
- private fun loadManifest(context: Context): SDKManifest? {
126
- return try {
127
- val json = context.assets.open("sdk-manifest.json").bufferedReader().readText()
128
- SDKManifest.fromJson(json)
129
- } catch (e: Exception) { null }
130
- }
131
-
132
- private fun sha256(file: File): String {
133
- val digest = MessageDigest.getInstance("SHA-256")
134
- file.inputStream().use { input ->
135
- val buffer = ByteArray(8192)
136
- var bytesRead: Int
137
- while (input.read(buffer).also { bytesRead = it } != -1) {
138
- digest.update(buffer, 0, bytesRead)
139
- }
140
- }
141
- return digest.digest().joinToString("") { "%02x".format(it) }
142
- }
143
-
144
- private fun unzip(source: File, destination: File) {
145
- ZipInputStream(source.inputStream()).use { zip ->
146
- var entry = zip.nextEntry
147
- while (entry != null) {
148
- val file = File(destination, entry.name)
149
- if (entry.isDirectory) {
150
- file.mkdirs()
151
- } else {
152
- file.parentFile?.mkdirs()
153
- file.outputStream().use { zip.copyTo(it) }
154
- }
155
- entry = zip.nextEntry
156
- }
157
- }
158
- }
159
- }
160
-
161
- data class SDKManifest(
162
- val version: String,
163
- val android: AndroidManifest
164
- ) {
165
- data class AndroidManifest(
166
- val playFeatureModule: String,
167
- val directDownload: DirectDownload
168
- )
169
-
170
- data class DirectDownload(
171
- val downloadUrl: String,
172
- val sha256: String,
173
- val sizeBytes: Long
174
- )
175
-
176
- companion object {
177
- fun fromJson(json: String): SDKManifest {
178
- val root = JSONObject(json)
179
- val android = root.getJSONObject("android")
180
- val direct = android.getJSONObject("directDownload")
181
-
182
- return SDKManifest(
183
- version = root.getString("version"),
184
- android = AndroidManifest(
185
- playFeatureModule = android.getString("playFeatureModule"),
186
- directDownload = DirectDownload(
187
- downloadUrl = direct.getString("downloadUrl"),
188
- sha256 = direct.getString("sha256"),
189
- sizeBytes = direct.getLong("sizeBytes")
190
- )
191
- )
192
- )
193
- }
194
- }
1
+ package flir.android
2
+
3
+ import android.content.Context
4
+ import android.os.Build
5
+ import android.util.Log
6
+ import kotlinx.coroutines.*
7
+ import org.json.JSONObject
8
+ import java.io.File
9
+ import java.io.FileOutputStream
10
+ import java.net.HttpURLConnection
11
+ import java.net.URL
12
+ import java.util.zip.ZipInputStream
13
+
14
+ /**
15
+ * FlirSDKLoader - Downloads and manages architecture-specific FLIR SDK packages
16
+ *
17
+ * Each package contains:
18
+ * - classes.dex (combined DEX from thermalsdk + androidsdk)
19
+ * - Native .so libraries in jni/{arch}/ folder
20
+ *
21
+ * URLs are read from sdk-manifest.json in assets folder
22
+ */
23
+ object FlirSDKLoader {
24
+
25
+ private const val TAG = "FlirSDKLoader"
26
+
27
+ // Cached manifest data
28
+ private var cachedManifest: SDKManifest? = null
29
+
30
+ // Manifest data classes
31
+ data class ArchPackage(val downloadUrl: String, val sizeBytes: Long)
32
+ data class SDKManifest(val version: String, val packages: Map<String, ArchPackage>)
33
+
34
+ private fun getSDKDirectory(context: Context) = File(context.filesDir, "FlirSDK")
35
+
36
+ /**
37
+ * Load manifest from assets
38
+ */
39
+ private fun loadManifest(context: Context): SDKManifest? {
40
+ if (cachedManifest != null) return cachedManifest
41
+
42
+ return try {
43
+ val json = context.assets.open("sdk-manifest.json").bufferedReader().readText()
44
+ val root = JSONObject(json)
45
+ val android = root.getJSONObject("android")
46
+ val packagesJson = android.getJSONObject("packages")
47
+
48
+ val packages = mutableMapOf<String, ArchPackage>()
49
+ packagesJson.keys().forEach { arch ->
50
+ val pkg = packagesJson.getJSONObject(arch)
51
+ packages[arch] = ArchPackage(
52
+ downloadUrl = pkg.getString("downloadUrl"),
53
+ sizeBytes = pkg.getLong("sizeBytes")
54
+ )
55
+ }
56
+
57
+ SDKManifest(
58
+ version = root.getString("version"),
59
+ packages = packages
60
+ ).also { cachedManifest = it }
61
+ } catch (e: Exception) {
62
+ Log.e(TAG, "Failed to load manifest: ${e.message}")
63
+ null
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Get the primary ABI for this device
69
+ */
70
+ fun getDeviceArch(): String {
71
+ val supportedAbis = Build.SUPPORTED_ABIS
72
+ Log.d(TAG, "Device supported ABIs: ${supportedAbis.joinToString()}")
73
+
74
+ val knownArchs = setOf("arm64-v8a", "armeabi-v7a", "x86_64")
75
+ for (abi in supportedAbis) {
76
+ if (abi in knownArchs) {
77
+ Log.d(TAG, "Selected ABI: $abi")
78
+ return abi
79
+ }
80
+ }
81
+ return "arm64-v8a"
82
+ }
83
+
84
+ /**
85
+ * Check if SDK is already downloaded
86
+ */
87
+ fun isSDKAvailable(context: Context): Boolean {
88
+ val sdkDir = getSDKDirectory(context)
89
+ val arch = getDeviceArch()
90
+
91
+ val dexFile = File(sdkDir, "classes.dex")
92
+ val soDir = File(sdkDir, "jni/$arch")
93
+
94
+ val hasDex = dexFile.exists() && dexFile.length() > 0
95
+ val hasSo = soDir.exists() && soDir.listFiles()?.isNotEmpty() == true
96
+
97
+ Log.d(TAG, "SDK available: dex=$hasDex, so=$hasSo")
98
+ return hasDex && hasSo
99
+ }
100
+
101
+ /**
102
+ * Get path to the DEX file
103
+ */
104
+ fun getDexPath(context: Context): File? {
105
+ val dexFile = File(getSDKDirectory(context), "classes.dex")
106
+ return if (dexFile.exists()) dexFile else null
107
+ }
108
+
109
+ /**
110
+ * Get path to native libraries directory
111
+ */
112
+ fun getNativeLibDir(context: Context): File? {
113
+ val arch = getDeviceArch()
114
+ val libDir = File(getSDKDirectory(context), "jni/$arch")
115
+ return if (libDir.exists()) libDir else null
116
+ }
117
+
118
+ /**
119
+ * Get estimated download size from manifest
120
+ */
121
+ fun getDownloadSize(context: Context): Long {
122
+ val manifest = loadManifest(context) ?: return 15_000_000L
123
+ val arch = getDeviceArch()
124
+ return manifest.packages[arch]?.sizeBytes ?: 15_000_000L
125
+ }
126
+
127
+ /**
128
+ * Download the SDK package for this device's architecture
129
+ */
130
+ suspend fun downloadSDK(
131
+ context: Context,
132
+ onProgress: (downloaded: Long, total: Long) -> Unit
133
+ ): Result<Unit> = withContext(Dispatchers.IO) {
134
+ try {
135
+ val arch = getDeviceArch()
136
+ val manifest = loadManifest(context)
137
+ ?: return@withContext Result.failure(Exception("Failed to load manifest"))
138
+
139
+ val archPackage = manifest.packages[arch]
140
+ ?: return@withContext Result.failure(Exception("No package for architecture: $arch"))
141
+
142
+ val downloadUrl = archPackage.downloadUrl
143
+ Log.i(TAG, "Downloading SDK for $arch from $downloadUrl")
144
+
145
+ val sdkDir = getSDKDirectory(context).apply { mkdirs() }
146
+ val zipFile = File(context.cacheDir, "flir-sdk-$arch.zip")
147
+
148
+ // Download with redirect handling (GitHub uses 302 redirects)
149
+ var connection = URL(downloadUrl).openConnection() as HttpURLConnection
150
+ connection.instanceFollowRedirects = true
151
+ connection.connectTimeout = 30000
152
+ connection.readTimeout = 60000
153
+
154
+ // Handle redirects manually for cross-protocol redirects
155
+ var redirectCount = 0
156
+ while (connection.responseCode in 301..302 && redirectCount < 5) {
157
+ val redirectUrl = connection.getHeaderField("Location")
158
+ Log.d(TAG, "Redirect to: $redirectUrl")
159
+ connection.disconnect()
160
+ connection = URL(redirectUrl).openConnection() as HttpURLConnection
161
+ connection.instanceFollowRedirects = true
162
+ redirectCount++
163
+ }
164
+
165
+ if (connection.responseCode != HttpURLConnection.HTTP_OK) {
166
+ return@withContext Result.failure(Exception("HTTP error ${connection.responseCode}"))
167
+ }
168
+
169
+ val totalSize = connection.contentLengthLong.let {
170
+ if (it > 0) it else archPackage.sizeBytes
171
+ }
172
+
173
+ connection.inputStream.use { input ->
174
+ FileOutputStream(zipFile).use { output ->
175
+ val buffer = ByteArray(8192)
176
+ var totalRead = 0L
177
+ var bytesRead: Int
178
+
179
+ while (input.read(buffer).also { bytesRead = it } != -1) {
180
+ output.write(buffer, 0, bytesRead)
181
+ totalRead += bytesRead
182
+ withContext(Dispatchers.Main) {
183
+ onProgress(totalRead, totalSize)
184
+ }
185
+ }
186
+ }
187
+ }
188
+ connection.disconnect()
189
+
190
+ Log.i(TAG, "Download complete: ${zipFile.length()} bytes")
191
+
192
+ // Extract
193
+ unzip(zipFile, sdkDir)
194
+ zipFile.delete()
195
+
196
+ // Verify
197
+ val dexFile = File(sdkDir, "classes.dex")
198
+ val soDir = File(sdkDir, "jni/$arch")
199
+
200
+ if (!dexFile.exists()) {
201
+ return@withContext Result.failure(Exception("DEX file not extracted"))
202
+ }
203
+
204
+ dexFile.setReadOnly()
205
+ Log.i(TAG, "SDK installed: dex=${dexFile.length()} bytes, libs=${soDir.listFiles()?.size ?: 0} files")
206
+
207
+ Result.success(Unit)
208
+ } catch (e: Exception) {
209
+ Log.e(TAG, "Download failed", e)
210
+ Result.failure(e)
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Delete downloaded SDK
216
+ */
217
+ fun deleteSDK(context: Context): Boolean {
218
+ return getSDKDirectory(context).deleteRecursively()
219
+ }
220
+
221
+ private fun unzip(source: File, destination: File) {
222
+ Log.d(TAG, "Extracting ${source.name} to ${destination.absolutePath}")
223
+
224
+ ZipInputStream(source.inputStream()).use { zip ->
225
+ var entry = zip.nextEntry
226
+ while (entry != null) {
227
+ val file = File(destination, entry.name)
228
+ Log.d(TAG, " Extracting: ${entry.name}")
229
+
230
+ if (entry.isDirectory) {
231
+ file.mkdirs()
232
+ } else {
233
+ file.parentFile?.mkdirs()
234
+ file.outputStream().use { output ->
235
+ zip.copyTo(output)
236
+ }
237
+ }
238
+ zip.closeEntry()
239
+ entry = zip.nextEntry
240
+ }
241
+ }
242
+ }
195
243
  }