ilabs-flir 1.0.2 → 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 (126) hide show
  1. package/Flir.podspec +31 -31
  2. package/README.md +1271 -1271
  3. package/android/Flir/build.gradle.kts +85 -80
  4. package/android/Flir/libs/flir-stubs.jar +0 -0
  5. package/android/Flir/src/main/AndroidManifest.xml +31 -31
  6. package/android/Flir/src/main/java/com/flir/thermalsdk/ErrorCode.java +13 -0
  7. package/android/Flir/src/main/java/com/flir/thermalsdk/ErrorCodeException.java +14 -0
  8. package/android/Flir/src/main/java/com/flir/thermalsdk/ThermalSdkAndroid.java +16 -0
  9. package/android/Flir/src/main/java/com/flir/thermalsdk/androidsdk/image/BitmapAndroid.java +20 -0
  10. package/android/Flir/src/main/java/com/flir/thermalsdk/image/ImageBuffer.java +11 -0
  11. package/android/Flir/src/main/java/com/flir/thermalsdk/image/JavaImageBuffer.java +35 -0
  12. package/android/Flir/src/main/java/com/flir/thermalsdk/image/Palette.java +15 -0
  13. package/android/Flir/src/main/java/com/flir/thermalsdk/image/PaletteManager.java +16 -0
  14. package/android/Flir/src/main/java/com/flir/thermalsdk/image/Point.java +11 -0
  15. package/android/Flir/src/main/java/com/flir/thermalsdk/image/ThermalImage.java +23 -0
  16. package/android/Flir/src/main/java/com/flir/thermalsdk/image/ThermalValue.java +9 -0
  17. package/android/Flir/src/main/java/com/flir/thermalsdk/live/Camera.java +26 -0
  18. package/android/Flir/src/main/java/com/flir/thermalsdk/live/CameraType.java +8 -0
  19. package/android/Flir/src/main/java/com/flir/thermalsdk/live/CommunicationInterface.java +16 -0
  20. package/android/Flir/src/main/java/com/flir/thermalsdk/live/ConnectParameters.java +16 -0
  21. package/android/Flir/src/main/java/com/flir/thermalsdk/live/Identity.java +23 -0
  22. package/android/Flir/src/main/java/com/flir/thermalsdk/live/IpSettings.java +9 -0
  23. package/android/Flir/src/main/java/com/flir/thermalsdk/live/RemoteControl.java +16 -0
  24. package/android/Flir/src/main/java/com/flir/thermalsdk/live/connectivity/ConnectionStatusListener.java +7 -0
  25. package/android/Flir/src/main/java/com/flir/thermalsdk/live/discovery/DiscoveryEventListener.java +14 -0
  26. package/android/Flir/src/main/java/com/flir/thermalsdk/live/discovery/DiscoveryFactory.java +33 -0
  27. package/android/Flir/src/main/java/com/flir/thermalsdk/live/remote/OnReceived.java +5 -0
  28. package/android/Flir/src/main/java/com/flir/thermalsdk/live/remote/OnRemoteError.java +7 -0
  29. package/android/Flir/src/main/java/com/flir/thermalsdk/live/streaming/Stream.java +8 -0
  30. package/android/Flir/src/main/java/com/flir/thermalsdk/live/streaming/ThermalStreamer.java +28 -0
  31. package/android/Flir/src/main/java/com/flir/thermalsdk/live/streaming/VisualStreamer.java +18 -0
  32. package/android/Flir/src/main/java/flir/android/FlirCommands.java +136 -0
  33. package/android/Flir/src/main/java/flir/android/FlirDownloadManager.kt +76 -75
  34. package/android/Flir/src/main/java/flir/android/FlirDownloadPackage.kt +16 -16
  35. package/android/Flir/src/main/java/flir/android/FlirFrameCache.kt +6 -6
  36. package/android/Flir/src/main/java/flir/android/FlirManager.kt +477 -248
  37. package/android/Flir/src/main/java/flir/android/FlirModule.kt +74 -74
  38. package/android/Flir/src/main/java/flir/android/FlirPackage.kt +19 -19
  39. package/android/Flir/src/main/java/flir/android/FlirSDKLoader.kt +165 -117
  40. package/android/Flir/src/main/java/flir/android/FlirSdkManager.java +1511 -0
  41. package/android/Flir/src/main/java/flir/android/FlirStatus.kt +12 -12
  42. package/android/Flir/src/main/java/flir/android/FlirView.kt +48 -48
  43. package/android/Flir/src/main/java/flir/android/FlirViewManager.kt +13 -13
  44. package/app.plugin.js +264 -264
  45. package/expo-module.config.json +5 -5
  46. package/ios/Flir/Framework/ThermalSDK/FLIRBattery.h +76 -76
  47. package/ios/Flir/Framework/ThermalSDK/FLIRCalibration.h +108 -108
  48. package/ios/Flir/Framework/ThermalSDK/FLIRCamera.h +156 -156
  49. package/ios/Flir/Framework/ThermalSDK/FLIRCameraDeviceInfo.h +53 -53
  50. package/ios/Flir/Framework/ThermalSDK/FLIRCameraEvent.h +132 -132
  51. package/ios/Flir/Framework/ThermalSDK/FLIRCameraImport.h +204 -204
  52. package/ios/Flir/Framework/ThermalSDK/FLIRColorDistributionSettings.h +204 -204
  53. package/ios/Flir/Framework/ThermalSDK/FLIRColorizer.h +82 -82
  54. package/ios/Flir/Framework/ThermalSDK/FLIRDiscoveredCamera.h +44 -44
  55. package/ios/Flir/Framework/ThermalSDK/FLIRDiscovery.h +132 -132
  56. package/ios/Flir/Framework/ThermalSDK/FLIRDisplaySettings.h +29 -29
  57. package/ios/Flir/Framework/ThermalSDK/FLIRFocus.h +70 -70
  58. package/ios/Flir/Framework/ThermalSDK/FLIRFusion.h +192 -192
  59. package/ios/Flir/Framework/ThermalSDK/FLIRFusionController.h +136 -136
  60. package/ios/Flir/Framework/ThermalSDK/FLIRFusionTransformation.h +35 -35
  61. package/ios/Flir/Framework/ThermalSDK/FLIRIdentity.h +264 -264
  62. package/ios/Flir/Framework/ThermalSDK/FLIRImageBase.h +196 -196
  63. package/ios/Flir/Framework/ThermalSDK/FLIRImageColorizer.h +26 -26
  64. package/ios/Flir/Framework/ThermalSDK/FLIRImageStatistics.h +61 -61
  65. package/ios/Flir/Framework/ThermalSDK/FLIRIsotherms.h +208 -208
  66. package/ios/Flir/Framework/ThermalSDK/FLIRMeasurementArea.h +38 -38
  67. package/ios/Flir/Framework/ThermalSDK/FLIRMeasurementCollection.h +147 -147
  68. package/ios/Flir/Framework/ThermalSDK/FLIRMeasurementDelta.h +62 -62
  69. package/ios/Flir/Framework/ThermalSDK/FLIRMeasurementDimensions.h +33 -33
  70. package/ios/Flir/Framework/ThermalSDK/FLIRMeasurementEllipse.h +49 -49
  71. package/ios/Flir/Framework/ThermalSDK/FLIRMeasurementLine.h +66 -66
  72. package/ios/Flir/Framework/ThermalSDK/FLIRMeasurementMarker.h +69 -69
  73. package/ios/Flir/Framework/ThermalSDK/FLIRMeasurementParameters.h +41 -41
  74. package/ios/Flir/Framework/ThermalSDK/FLIRMeasurementRectangle.h +36 -36
  75. package/ios/Flir/Framework/ThermalSDK/FLIRMeasurementReference.h +27 -27
  76. package/ios/Flir/Framework/ThermalSDK/FLIRMeasurementShape.h +46 -46
  77. package/ios/Flir/Framework/ThermalSDK/FLIRMeasurementSpot.h +33 -33
  78. package/ios/Flir/Framework/ThermalSDK/FLIRMeasurementsController.h +160 -160
  79. package/ios/Flir/Framework/ThermalSDK/FLIRMeterLinkSensorPoll.h +247 -247
  80. package/ios/Flir/Framework/ThermalSDK/FLIROverlayController.h +27 -27
  81. package/ios/Flir/Framework/ThermalSDK/FLIRPalette.h +60 -60
  82. package/ios/Flir/Framework/ThermalSDK/FLIRPaletteController.h +36 -36
  83. package/ios/Flir/Framework/ThermalSDK/FLIRPaletteManager.h +97 -97
  84. package/ios/Flir/Framework/ThermalSDK/FLIRQuantification.h +55 -55
  85. package/ios/Flir/Framework/ThermalSDK/FLIRRemoteControl.h +393 -393
  86. package/ios/Flir/Framework/ThermalSDK/FLIRRenderer.h +35 -35
  87. package/ios/Flir/Framework/ThermalSDK/FLIRRendererImpl.h +17 -17
  88. package/ios/Flir/Framework/ThermalSDK/FLIRScale.h +99 -99
  89. package/ios/Flir/Framework/ThermalSDK/FLIRScaleController.h +44 -44
  90. package/ios/Flir/Framework/ThermalSDK/FLIRStream.h +109 -109
  91. package/ios/Flir/Framework/ThermalSDK/FLIRStreamer.h +124 -124
  92. package/ios/Flir/Framework/ThermalSDK/FLIRSystem.h +40 -40
  93. package/ios/Flir/Framework/ThermalSDK/FLIRTemperatureRange.h +43 -43
  94. package/ios/Flir/Framework/ThermalSDK/FLIRThermalDelta.h +77 -77
  95. package/ios/Flir/Framework/ThermalSDK/FLIRThermalImage.h +331 -331
  96. package/ios/Flir/Framework/ThermalSDK/FLIRThermalImageFile.h +56 -56
  97. package/ios/Flir/Framework/ThermalSDK/FLIRThermalParameters.h +31 -31
  98. package/ios/Flir/Framework/ThermalSDK/FLIRThermalValue.h +92 -92
  99. package/ios/Flir/Framework/ThermalSDK/FLIRWirelessCameraDetails.h +88 -88
  100. package/ios/Flir/Framework/ThermalSDK/ThermalSDK.h +73 -73
  101. package/ios/Flir/SDKLoader/FlirSDKLoader.m +13 -13
  102. package/ios/Flir/SDKLoader/FlirSDKLoader.swift +175 -175
  103. package/ios/Flir/src/FlirEventEmitter.h +12 -12
  104. package/ios/Flir/src/FlirEventEmitter.m +33 -33
  105. package/ios/Flir/src/FlirModule.h +10 -10
  106. package/ios/Flir/src/FlirModule.m +381 -381
  107. package/ios/Flir/src/FlirPreviewView.h +13 -13
  108. package/ios/Flir/src/FlirPreviewView.m +24 -24
  109. package/ios/Flir/src/FlirState.h +20 -20
  110. package/ios/Flir/src/FlirState.m +79 -79
  111. package/ios/Flir/src/FlirViewManager.h +9 -9
  112. package/ios/Flir/src/FlirViewManager.m +16 -16
  113. package/package.json +60 -60
  114. package/react-native.config.js +14 -14
  115. package/scripts/copy_ios_libs.sh +32 -32
  116. package/scripts/create_stubs.py +174 -174
  117. package/scripts/download-sdk.js +62 -62
  118. package/scripts/prepare-binaries.sh +171 -171
  119. package/sdk-manifest.json +37 -30
  120. package/src/FlirDownload.ts +78 -78
  121. package/src/index.d.ts +17 -17
  122. package/src/index.js +7 -7
  123. package/src/index.ts +7 -7
  124. package/android/Flir/src/main/java/flir/android/CameraHandler.java +0 -194
  125. package/android/Flir/src/main/java/flir/android/FlirController.kt +0 -11
  126. package/android/Flir/src/main/java/flir/android/FrameDataHolder.java +0 -14
@@ -0,0 +1,1511 @@
1
+ package flir.android;
2
+
3
+ import android.graphics.Bitmap;
4
+ import android.os.Handler;
5
+ import android.os.Looper;
6
+ import android.util.Log;
7
+
8
+ import java.io.File;
9
+ import java.io.FileOutputStream;
10
+ import java.lang.reflect.Method;
11
+ import java.lang.reflect.Proxy;
12
+ import java.util.ArrayList;
13
+ import java.util.List;
14
+ import java.util.concurrent.CopyOnWriteArrayList;
15
+ import java.util.concurrent.Executors;
16
+ import java.util.concurrent.ScheduledExecutorService;
17
+ import java.util.concurrent.ScheduledFuture;
18
+ import java.util.concurrent.TimeUnit;
19
+ import java.util.concurrent.atomic.AtomicBoolean;
20
+ import java.util.zip.ZipEntry;
21
+ import java.util.zip.ZipFile;
22
+
23
+ import dalvik.system.DexClassLoader;
24
+
25
+ /**
26
+ * FLIR SDK Manager - Handles device discovery, connection, and streaming using reflection.
27
+ * Supports USB, NETWORK (FLIR ONE Edge), and EMULATOR interfaces.
28
+ * All SDK calls use reflection for loose binding - no compile-time dependency on FLIR SDK.
29
+ *
30
+ * Flow:
31
+ * isFlir=true → startDiscovery()
32
+ * ↓ disconnect current device (if any)
33
+ * ↓ (timeout: 0s if isEmu=true, 5s otherwise)
34
+ * ├─ Devices found → emit to RN → stream from device[0]
35
+ * │ ↓ RN sends deviceId
36
+ * │ └─ disconnect → switch to deviceId
37
+ * ├─ No device OR isEmu=true → emulatorDiscovery()
38
+ * │ ↓ Check emulator type setting
39
+ * │ ├─ FLIR_ONE_EDGE (default)
40
+ * │ └─ FLIR_ONE
41
+ * │ └─ stream from emulator
42
+ * └─ Streaming: connect → start stream → upload to texture
43
+ * ↓ acol changes → setPalette()
44
+ * ↓ touch point → getTemperatureAt(x,y)
45
+ */
46
+ public class FlirSdkManager {
47
+ private static final String TAG = "FlirSdkManager";
48
+ private static final String FLOW_TAG = "FLIR_FLOW"; // For step-by-step flow logging
49
+
50
+ // Step counter for tracking flow
51
+ private int stepCounter = 0;
52
+ private long flowStartTime = 0;
53
+
54
+ private void logStep(String step, String details) {
55
+ stepCounter++;
56
+ long elapsed = flowStartTime > 0 ? System.currentTimeMillis() - flowStartTime : 0;
57
+ String msg = String.format("[Step %d] [+%dms] %s: %s", stepCounter, elapsed, step, details);
58
+ Log.i(FLOW_TAG, msg);
59
+ }
60
+
61
+ private void resetFlowTracking() {
62
+ stepCounter = 0;
63
+ flowStartTime = System.currentTimeMillis();
64
+ Log.i(FLOW_TAG, "========== FLIR FLOW STARTED ==========");
65
+ }
66
+
67
+ // Discovery timeout in milliseconds
68
+ private static final long DISCOVERY_TIMEOUT_DEVICE_MS = 5000; // 5 seconds for real devices
69
+ private static final long DISCOVERY_TIMEOUT_EMULATOR_MS = 0; // Immediate for emulator mode
70
+
71
+ // Emulator types
72
+ public enum EmulatorType {
73
+ FLIR_ONE_EDGE, // Default - WiFi emulator
74
+ FLIR_ONE // USB emulator
75
+ }
76
+
77
+ // Communication interfaces (mirrors SDK enum)
78
+ public enum CommInterface {
79
+ USB,
80
+ NETWORK,
81
+ EMULATOR
82
+ }
83
+
84
+ // Listener interface for callbacks
85
+ public interface Listener {
86
+ void onFrame(Bitmap bitmap);
87
+ void onTemperature(double temp, int x, int y);
88
+ void onDeviceFound(String deviceId, String deviceName, boolean isEmulator);
89
+ void onDeviceListUpdated(List<DeviceInfo> devices);
90
+ void onDeviceConnected(String deviceId, String deviceName, boolean isEmulator);
91
+ void onDeviceDisconnected();
92
+ void onDiscoveryStarted();
93
+ void onDiscoveryTimeout();
94
+ void onStreamStarted(String streamType);
95
+ void onError(String error);
96
+ }
97
+
98
+ // Device info class for discovered devices
99
+ public static class DeviceInfo {
100
+ public final String deviceId;
101
+ public final String deviceName;
102
+ public final boolean isEmulator;
103
+ public final CommInterface commInterface;
104
+ public final Object identity; // SDK Identity object (kept for connection)
105
+
106
+ DeviceInfo(String id, String name, boolean emu, CommInterface iface, Object identity) {
107
+ this.deviceId = id;
108
+ this.deviceName = name;
109
+ this.isEmulator = emu;
110
+ this.commInterface = iface;
111
+ this.identity = identity;
112
+ }
113
+ }
114
+
115
+ private final Listener listener;
116
+ private final android.content.Context appContext;
117
+ private final Handler mainHandler = new Handler(Looper.getMainLooper());
118
+ private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
119
+
120
+ // SDK objects (all via reflection)
121
+ private ClassLoader sdkClassLoader = null;
122
+ private Object discoveryFactory = null;
123
+ private Object discoveryListener = null;
124
+ private Object cameraObj = null;
125
+ private Object streamerObj = null;
126
+ private Object currentStream = null;
127
+ private Object currentPalette = null;
128
+
129
+ // State tracking
130
+ private final AtomicBoolean isDiscovering = new AtomicBoolean(false);
131
+ private final AtomicBoolean isConnected = new AtomicBoolean(false);
132
+ private final AtomicBoolean isStreaming = new AtomicBoolean(false);
133
+ private final AtomicBoolean isEmulatorMode = new AtomicBoolean(false);
134
+ private final CopyOnWriteArrayList<DeviceInfo> discoveredDevices = new CopyOnWriteArrayList<>();
135
+ private ScheduledFuture<?> discoveryTimeoutFuture = null;
136
+ private DeviceInfo connectedDevice = null;
137
+ private EmulatorType emulatorType = EmulatorType.FLIR_ONE_EDGE;
138
+
139
+ // Frame state
140
+ private volatile Bitmap latestFrame = null;
141
+ private String sdkJarPath = null;
142
+ private String currentStreamKind = null;
143
+
144
+ // Frame counting for debug logging
145
+ private int frameCount = 0;
146
+ private long lastFrameLogTime = 0;
147
+ private int successfulBitmapCount = 0;
148
+
149
+ FlirSdkManager(Listener listener, android.content.Context context) {
150
+ this.listener = listener;
151
+ this.appContext = context != null ? context.getApplicationContext() : null;
152
+ }
153
+
154
+ // ==================== PUBLIC API ====================
155
+
156
+ /**
157
+ * Set the emulator type to use when no physical device is found
158
+ */
159
+ public void setEmulatorType(EmulatorType type) {
160
+ this.emulatorType = type;
161
+ Log.i(TAG, "[FLIR] Emulator type set to: " + type);
162
+ logStep("SET_EMULATOR_TYPE", "type=" + type + " (FLIR_ONE_EDGE=WiFi, FLIR_ONE=USB)");
163
+ }
164
+
165
+ /**
166
+ * Start device discovery. Will disconnect current device first.
167
+ * @param forceEmulator If true, skip device discovery and connect to emulator immediately
168
+ */
169
+ public void startDiscovery(boolean forceEmulator) {
170
+ resetFlowTracking();
171
+ logStep("START_DISCOVERY", "forceEmulator=" + forceEmulator + ", emulatorType=" + emulatorType);
172
+ Log.i(TAG, "[FLIR] startDiscovery(forceEmulator=" + forceEmulator + ")");
173
+
174
+ // Always disconnect first
175
+ if (isConnected.get()) {
176
+ logStep("DISCONNECT_PREVIOUS", "Disconnecting current device before discovery");
177
+ disconnect();
178
+ }
179
+
180
+ // Clear discovered devices
181
+ discoveredDevices.clear();
182
+ logStep("CLEAR_DEVICES", "Cleared discovered devices list");
183
+
184
+ if (forceEmulator) {
185
+ // Immediate emulator mode
186
+ logStep("MODE_EMULATOR", "Forcing emulator mode - skipping device discovery");
187
+ isEmulatorMode.set(true);
188
+ startEmulatorDiscovery();
189
+ } else {
190
+ // Normal discovery with timeout
191
+ logStep("MODE_FULL_DISCOVERY", "Starting full discovery (USB+NETWORK+EMULATOR), timeout=" + DISCOVERY_TIMEOUT_DEVICE_MS + "ms");
192
+ isEmulatorMode.set(false);
193
+ startFullDiscovery();
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Stop discovery scan
199
+ */
200
+ public void stopDiscovery() {
201
+ Log.i(TAG, "[FLIR] stopDiscovery()");
202
+ cancelDiscoveryTimeout();
203
+ isDiscovering.set(false);
204
+
205
+ try {
206
+ if (discoveryFactory != null) {
207
+ Method stopMethod = discoveryFactory.getClass().getMethod("stop");
208
+ stopMethod.invoke(discoveryFactory);
209
+ }
210
+ } catch (Throwable t) {
211
+ Log.w(TAG, "[FLIR] stopDiscovery failed: " + t.getMessage());
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Connect to a specific device by ID
217
+ */
218
+ public void connectToDevice(String deviceId) {
219
+ Log.i(TAG, "[FLIR] connectToDevice: " + deviceId);
220
+
221
+ // Find device in discovered list
222
+ DeviceInfo target = null;
223
+ for (DeviceInfo d : discoveredDevices) {
224
+ if (d.deviceId.equals(deviceId)) {
225
+ target = d;
226
+ break;
227
+ }
228
+ }
229
+
230
+ if (target == null) {
231
+ notifyError("Device not found: " + deviceId);
232
+ return;
233
+ }
234
+
235
+ // Disconnect current if needed
236
+ if (isConnected.get()) {
237
+ disconnect();
238
+ }
239
+
240
+ // Connect to target
241
+ connectToIdentity(target);
242
+ }
243
+
244
+ /**
245
+ * Disconnect current device/emulator
246
+ */
247
+ public void disconnect() {
248
+ Log.i(TAG, "[FLIR] disconnect()");
249
+
250
+ // Stop streaming first
251
+ stopStreaming();
252
+
253
+ // Disconnect camera
254
+ if (cameraObj != null) {
255
+ try {
256
+ Method disconnectMethod = cameraObj.getClass().getMethod("disconnect");
257
+ disconnectMethod.invoke(cameraObj);
258
+ Log.i(TAG, "[FLIR] Camera disconnected");
259
+ } catch (Throwable t) {
260
+ Log.w(TAG, "[FLIR] disconnect failed: " + t.getMessage());
261
+ }
262
+ }
263
+
264
+ cameraObj = null;
265
+ streamerObj = null;
266
+ currentStream = null;
267
+ connectedDevice = null;
268
+ isConnected.set(false);
269
+ isStreaming.set(false);
270
+
271
+ if (listener != null) {
272
+ mainHandler.post(() -> listener.onDeviceDisconnected());
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Set palette by name (iron, rainbow, etc.)
278
+ */
279
+ public void setPalette(String paletteName) {
280
+ Log.d(TAG, "[FLIR] setPalette: " + paletteName);
281
+
282
+ if (streamerObj == null) {
283
+ Log.w(TAG, "[FLIR] Cannot set palette - no active streamer");
284
+ return;
285
+ }
286
+
287
+ scheduler.submit(() -> {
288
+ try {
289
+ // Get PaletteManager.getDefaultPalettes()
290
+ Class<?> paletteManagerClass = findSdkClass("com.flir.thermalsdk.image.PaletteManager");
291
+ Method getDefaultPalettes = paletteManagerClass.getMethod("getDefaultPalettes");
292
+ Object palettes = getDefaultPalettes.invoke(null);
293
+
294
+ // Find matching palette
295
+ Object targetPalette = null;
296
+ if (palettes instanceof List) {
297
+ for (Object p : (List<?>) palettes) {
298
+ Method getName = p.getClass().getMethod("getName");
299
+ String name = (String) getName.invoke(p);
300
+ if (name != null && name.equalsIgnoreCase(paletteName)) {
301
+ targetPalette = p;
302
+ break;
303
+ }
304
+ }
305
+ }
306
+
307
+ if (targetPalette != null) {
308
+ currentPalette = targetPalette;
309
+ Log.i(TAG, "[FLIR] Palette set to: " + paletteName);
310
+ } else {
311
+ Log.w(TAG, "[FLIR] Palette not found: " + paletteName);
312
+ }
313
+ } catch (Throwable t) {
314
+ Log.w(TAG, "[FLIR] setPalette failed: " + t.getMessage());
315
+ }
316
+ });
317
+ }
318
+
319
+ /**
320
+ * Initialize default "iron" palette for thermal streaming.
321
+ * Called automatically when thermal streaming starts if no palette is set.
322
+ */
323
+ private void initializeDefaultPalette() {
324
+ try {
325
+ Class<?> paletteManagerClass = findSdkClass("com.flir.thermalsdk.image.PaletteManager");
326
+ Method getDefaultPalettes = paletteManagerClass.getMethod("getDefaultPalettes");
327
+ Object palettes = getDefaultPalettes.invoke(null);
328
+
329
+ if (palettes instanceof List) {
330
+ // Try to find "iron" palette first, then fall back to first available
331
+ Object ironPalette = null;
332
+ Object firstPalette = null;
333
+
334
+ for (Object p : (List<?>) palettes) {
335
+ if (firstPalette == null) firstPalette = p;
336
+
337
+ try {
338
+ Method getName = p.getClass().getMethod("getName");
339
+ String name = (String) getName.invoke(p);
340
+ if (name != null && name.toLowerCase().contains("iron")) {
341
+ ironPalette = p;
342
+ break;
343
+ }
344
+ } catch (Throwable ignored) {}
345
+ }
346
+
347
+ currentPalette = ironPalette != null ? ironPalette : firstPalette;
348
+
349
+ if (currentPalette != null) {
350
+ try {
351
+ Method getName = currentPalette.getClass().getMethod("getName");
352
+ String name = (String) getName.invoke(currentPalette);
353
+ Log.i(TAG, "[FLIR] Default palette initialized: " + name);
354
+ } catch (Throwable ignored) {
355
+ Log.i(TAG, "[FLIR] Default palette initialized");
356
+ }
357
+ }
358
+ }
359
+ } catch (Throwable t) {
360
+ Log.w(TAG, "[FLIR] initializeDefaultPalette failed: " + t.getMessage());
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Get temperature at a specific point
366
+ */
367
+ public double getTemperatureAtPoint(int x, int y) {
368
+ if (streamerObj == null) return Double.NaN;
369
+
370
+ try {
371
+ // Get the thermal image from streamer
372
+ Method getImage = streamerObj.getClass().getMethod("getImage");
373
+ Object thermalImage = getImage.invoke(streamerObj);
374
+
375
+ if (thermalImage != null) {
376
+ // Try getValueAt(Point)
377
+ try {
378
+ Method getValueAt = thermalImage.getClass().getMethod("getValueAt", android.graphics.Point.class);
379
+ android.graphics.Point p = new android.graphics.Point(x, y);
380
+ Object temp = getValueAt.invoke(thermalImage, p);
381
+ if (temp instanceof Double) return (Double) temp;
382
+ if (temp instanceof Float) return ((Float) temp).doubleValue();
383
+ } catch (NoSuchMethodException ignored) {}
384
+
385
+ // Try getValues().getValueAt(x, y)
386
+ try {
387
+ Method getValues = thermalImage.getClass().getMethod("getValues");
388
+ Object values = getValues.invoke(thermalImage);
389
+ if (values != null) {
390
+ Method valGetAt = values.getClass().getMethod("getValueAt", int.class, int.class);
391
+ Object temp = valGetAt.invoke(values, x, y);
392
+ if (temp instanceof Double) return (Double) temp;
393
+ }
394
+ } catch (Throwable ignored) {}
395
+ }
396
+ } catch (Throwable t) {
397
+ Log.d(TAG, "[FLIR] getTemperatureAtPoint failed: " + t.getMessage());
398
+ }
399
+
400
+ return Double.NaN;
401
+ }
402
+
403
+ /**
404
+ * Get latest frame bitmap
405
+ */
406
+ public Bitmap getLatestFrame() {
407
+ return latestFrame;
408
+ }
409
+
410
+ /**
411
+ * Check if streaming is active
412
+ */
413
+ public boolean isStreamingActive() {
414
+ return isStreaming.get();
415
+ }
416
+
417
+ /**
418
+ * Check if connected
419
+ */
420
+ public boolean isConnected() {
421
+ return isConnected.get();
422
+ }
423
+
424
+ /**
425
+ * Get current stream type
426
+ */
427
+ public String getCurrentStreamKind() {
428
+ return currentStreamKind;
429
+ }
430
+
431
+ /**
432
+ * Get list of discovered devices
433
+ */
434
+ public List<DeviceInfo> getDiscoveredDevices() {
435
+ return new ArrayList<>(discoveredDevices);
436
+ }
437
+
438
+ /**
439
+ * Cleanup resources
440
+ */
441
+ public void stop() {
442
+ Log.i(TAG, "[FLIR] stop()");
443
+ stopDiscovery();
444
+ disconnect();
445
+ scheduler.shutdownNow();
446
+ }
447
+
448
+ // ==================== DISCOVERY IMPLEMENTATION ====================
449
+
450
+ private void startFullDiscovery() {
451
+ Log.i(TAG, "[FLIR] Starting full discovery (USB, NETWORK, EMULATOR)");
452
+
453
+ if (!initializeSdk()) {
454
+ Log.w(TAG, "[FLIR] SDK not available, falling back to emulator");
455
+ startEmulatorDiscovery();
456
+ return;
457
+ }
458
+
459
+ isDiscovering.set(true);
460
+ if (listener != null) {
461
+ mainHandler.post(() -> listener.onDiscoveryStarted());
462
+ }
463
+
464
+ try {
465
+ // Get DiscoveryFactory.getInstance()
466
+ Class<?> discoveryFactoryClass = findSdkClass("com.flir.thermalsdk.live.discovery.DiscoveryFactory");
467
+ Method getInstance = discoveryFactoryClass.getMethod("getInstance");
468
+ discoveryFactory = getInstance.invoke(null);
469
+
470
+ // Get CommunicationInterface enum values
471
+ Class<?> commIfaceClass = findSdkClass("com.flir.thermalsdk.live.CommunicationInterface");
472
+ Object usbInterface = Enum.valueOf((Class<Enum>) commIfaceClass, "USB");
473
+ Object networkInterface = Enum.valueOf((Class<Enum>) commIfaceClass, "NETWORK");
474
+ Object emulatorInterface = Enum.valueOf((Class<Enum>) commIfaceClass, "EMULATOR");
475
+
476
+ // Create discovery listener proxy
477
+ Class<?> listenerClass = findSdkClass("com.flir.thermalsdk.live.discovery.DiscoveryEventListener");
478
+ discoveryListener = Proxy.newProxyInstance(
479
+ getEffectiveClassLoader(),
480
+ new Class<?>[] { listenerClass },
481
+ (proxy, method, args) -> handleDiscoveryCallback(method.getName(), args)
482
+ );
483
+
484
+ // Create interface array [USB, NETWORK, EMULATOR]
485
+ Object ifaceArray = java.lang.reflect.Array.newInstance(commIfaceClass, 3);
486
+ java.lang.reflect.Array.set(ifaceArray, 0, usbInterface);
487
+ java.lang.reflect.Array.set(ifaceArray, 1, networkInterface);
488
+ java.lang.reflect.Array.set(ifaceArray, 2, emulatorInterface);
489
+
490
+ // Start discovery scan
491
+ Method scanMethod = discoveryFactoryClass.getMethod("scan", listenerClass, ifaceArray.getClass());
492
+ scanMethod.invoke(discoveryFactory, discoveryListener, ifaceArray);
493
+
494
+ Log.i(TAG, "[FLIR] Discovery scan started for USB, NETWORK, EMULATOR");
495
+
496
+ // Set discovery timeout
497
+ startDiscoveryTimeout(DISCOVERY_TIMEOUT_DEVICE_MS);
498
+
499
+ } catch (Throwable t) {
500
+ Log.e(TAG, "[FLIR] startFullDiscovery failed: " + t.getMessage(), t);
501
+ notifyError("Discovery failed: " + t.getMessage());
502
+ // Fallback to emulator
503
+ startEmulatorDiscovery();
504
+ }
505
+ }
506
+
507
+ private void startEmulatorDiscovery() {
508
+ logStep("EMULATOR_DISCOVERY_START", "type=" + emulatorType);
509
+ Log.i(TAG, "[FLIR] Starting emulator discovery (type=" + emulatorType + ")");
510
+
511
+ if (!initializeSdk()) {
512
+ logStep("SDK_INIT_FAILED", "FLIR SDK not available - check if SDK JAR is loaded");
513
+ notifyError("FLIR SDK not available");
514
+ return;
515
+ }
516
+ logStep("SDK_INITIALIZED", "FLIR SDK successfully initialized");
517
+
518
+ isDiscovering.set(true);
519
+ isEmulatorMode.set(true);
520
+
521
+ try {
522
+ // Get DiscoveryFactory
523
+ Class<?> discoveryFactoryClass = findSdkClass("com.flir.thermalsdk.live.discovery.DiscoveryFactory");
524
+ Method getInstance = discoveryFactoryClass.getMethod("getInstance");
525
+ discoveryFactory = getInstance.invoke(null);
526
+
527
+ // Get EMULATOR interface
528
+ Class<?> commIfaceClass = findSdkClass("com.flir.thermalsdk.live.CommunicationInterface");
529
+ Object emulatorInterface = Enum.valueOf((Class<Enum>) commIfaceClass, "EMULATOR");
530
+
531
+ // Create discovery listener
532
+ Class<?> listenerClass = findSdkClass("com.flir.thermalsdk.live.discovery.DiscoveryEventListener");
533
+ discoveryListener = Proxy.newProxyInstance(
534
+ getEffectiveClassLoader(),
535
+ new Class<?>[] { listenerClass },
536
+ (proxy, method, args) -> handleDiscoveryCallback(method.getName(), args)
537
+ );
538
+
539
+ // Create interface array [EMULATOR]
540
+ Object ifaceArray = java.lang.reflect.Array.newInstance(commIfaceClass, 1);
541
+ java.lang.reflect.Array.set(ifaceArray, 0, emulatorInterface);
542
+
543
+ // Start discovery
544
+ Method scanMethod = discoveryFactoryClass.getMethod("scan", listenerClass, ifaceArray.getClass());
545
+ scanMethod.invoke(discoveryFactory, discoveryListener, ifaceArray);
546
+
547
+ logStep("EMULATOR_SCAN_STARTED", "Scanning for emulator devices...");
548
+ Log.i(TAG, "[FLIR] Emulator discovery started");
549
+
550
+ } catch (Throwable t) {
551
+ logStep("EMULATOR_DISCOVERY_ERROR", "Failed: " + t.getMessage());
552
+ Log.e(TAG, "[FLIR] startEmulatorDiscovery failed: " + t.getMessage(), t);
553
+ notifyError("Emulator discovery failed: " + t.getMessage());
554
+ }
555
+ }
556
+
557
+ private Object handleDiscoveryCallback(String methodName, Object[] args) {
558
+ switch (methodName) {
559
+ case "onCameraFound":
560
+ if (args != null && args.length > 0) {
561
+ handleCameraFound(args[0]);
562
+ }
563
+ break;
564
+
565
+ case "onCameraLost":
566
+ if (args != null && args.length > 0) {
567
+ handleCameraLost(args[0]);
568
+ }
569
+ break;
570
+
571
+ case "onDiscoveryError":
572
+ if (args != null && args.length > 1) {
573
+ Log.w(TAG, "[FLIR] Discovery error: " + args[1]);
574
+ }
575
+ break;
576
+ }
577
+ return null;
578
+ }
579
+
580
+ private void handleCameraFound(Object discoveredCamera) {
581
+ try {
582
+ // Get identity from DiscoveredCamera
583
+ Method getIdentity = discoveredCamera.getClass().getMethod("getIdentity");
584
+ Object identity = getIdentity.invoke(discoveredCamera);
585
+
586
+ // Extract device info from identity
587
+ String deviceId = extractDeviceId(identity);
588
+ String deviceName = extractDeviceName(identity);
589
+ CommInterface commInterface = extractCommInterface(identity);
590
+ boolean isEmulator = commInterface == CommInterface.EMULATOR;
591
+
592
+ logStep("DEVICE_FOUND", "name=" + deviceName + ", id=" + deviceId + ", interface=" + commInterface + ", isEmulator=" + isEmulator);
593
+ Log.i(TAG, "[FLIR] Camera found: " + deviceName + " (" + commInterface + ")");
594
+
595
+ // Create device info
596
+ DeviceInfo deviceInfo = new DeviceInfo(deviceId, deviceName, isEmulator, commInterface, identity);
597
+
598
+ // Add to list if not already present
599
+ boolean exists = false;
600
+ for (DeviceInfo d : discoveredDevices) {
601
+ if (d.deviceId.equals(deviceId)) {
602
+ exists = true;
603
+ break;
604
+ }
605
+ }
606
+
607
+ if (!exists) {
608
+ discoveredDevices.add(deviceInfo);
609
+
610
+ // Notify listener
611
+ if (listener != null) {
612
+ final List<DeviceInfo> devices = new ArrayList<>(discoveredDevices);
613
+ mainHandler.post(() -> {
614
+ listener.onDeviceFound(deviceId, deviceName, isEmulator);
615
+ listener.onDeviceListUpdated(devices);
616
+ });
617
+ }
618
+
619
+ // If this is first device found (not emulator in normal mode), connect to it
620
+ if (!isEmulator && discoveredDevices.size() == 1 && !isEmulatorMode.get()) {
621
+ cancelDiscoveryTimeout();
622
+ stopDiscovery();
623
+ connectToIdentity(deviceInfo);
624
+ } else if (isEmulator && isEmulatorMode.get()) {
625
+ // In emulator mode, connect to first emulator found
626
+ stopDiscovery();
627
+ connectToIdentity(deviceInfo);
628
+ }
629
+ }
630
+
631
+ } catch (Throwable t) {
632
+ Log.e(TAG, "[FLIR] handleCameraFound failed: " + t.getMessage(), t);
633
+ }
634
+ }
635
+
636
+ private void handleCameraLost(Object discoveredCamera) {
637
+ try {
638
+ Method getIdentity = discoveredCamera.getClass().getMethod("getIdentity");
639
+ Object identity = getIdentity.invoke(discoveredCamera);
640
+ String deviceId = extractDeviceId(identity);
641
+
642
+ Log.i(TAG, "[FLIR] Camera lost: " + deviceId);
643
+
644
+ // Remove from list
645
+ discoveredDevices.removeIf(d -> d.deviceId.equals(deviceId));
646
+
647
+ // If this was our connected device, disconnect
648
+ if (connectedDevice != null && connectedDevice.deviceId.equals(deviceId)) {
649
+ disconnect();
650
+ }
651
+
652
+ } catch (Throwable t) {
653
+ Log.w(TAG, "[FLIR] handleCameraLost failed: " + t.getMessage());
654
+ }
655
+ }
656
+
657
+ private void startDiscoveryTimeout(long timeoutMs) {
658
+ cancelDiscoveryTimeout();
659
+
660
+ if (timeoutMs <= 0) return;
661
+
662
+ discoveryTimeoutFuture = scheduler.schedule(() -> {
663
+ Log.i(TAG, "[FLIR] Discovery timeout");
664
+ isDiscovering.set(false);
665
+
666
+ if (listener != null) {
667
+ mainHandler.post(() -> listener.onDiscoveryTimeout());
668
+ }
669
+
670
+ // If no physical devices found, try emulator
671
+ boolean hasPhysicalDevice = false;
672
+ for (DeviceInfo d : discoveredDevices) {
673
+ if (!d.isEmulator) {
674
+ hasPhysicalDevice = true;
675
+ break;
676
+ }
677
+ }
678
+
679
+ if (!hasPhysicalDevice) {
680
+ Log.i(TAG, "[FLIR] No physical devices found, starting emulator");
681
+ startEmulatorDiscovery();
682
+ } else if (!discoveredDevices.isEmpty()) {
683
+ // Connect to first device
684
+ connectToIdentity(discoveredDevices.get(0));
685
+ }
686
+
687
+ }, timeoutMs, TimeUnit.MILLISECONDS);
688
+ }
689
+
690
+ private void cancelDiscoveryTimeout() {
691
+ if (discoveryTimeoutFuture != null) {
692
+ discoveryTimeoutFuture.cancel(false);
693
+ discoveryTimeoutFuture = null;
694
+ }
695
+ }
696
+
697
+ // ==================== CONNECTION IMPLEMENTATION ====================
698
+
699
+ private void connectToIdentity(DeviceInfo deviceInfo) {
700
+ logStep("CONNECT_START", "Connecting to: " + deviceInfo.deviceName + " (" + deviceInfo.commInterface + ")");
701
+ Log.i(TAG, "[FLIR] Connecting to: " + deviceInfo.deviceName);
702
+
703
+ scheduler.submit(() -> {
704
+ try {
705
+ // Create Camera instance
706
+ logStep("CREATE_CAMERA", "Creating Camera instance");
707
+ Class<?> cameraClass = findSdkClass("com.flir.thermalsdk.live.Camera");
708
+ cameraObj = cameraClass.newInstance();
709
+
710
+ // Create connection status listener
711
+ Class<?> connStatusClass = findSdkClass("com.flir.thermalsdk.live.connectivity.ConnectionStatusListener");
712
+ Object connListener = Proxy.newProxyInstance(
713
+ getEffectiveClassLoader(),
714
+ new Class<?>[] { connStatusClass },
715
+ (proxy, method, args) -> {
716
+ if ("onDisconnected".equals(method.getName())) {
717
+ Log.w(TAG, "[FLIR] Camera disconnected (callback)");
718
+ handleDisconnected();
719
+ }
720
+ return null;
721
+ }
722
+ );
723
+
724
+ // Try to connect with different method signatures
725
+ boolean connected = false;
726
+
727
+ // Try connect(Identity, ConnectionStatusListener, ConnectParameters)
728
+ try {
729
+ Class<?> connectParamsClass = findSdkClass("com.flir.thermalsdk.live.ConnectParameters");
730
+ Object connectParams = connectParamsClass.newInstance();
731
+ Class<?> identityClass = findSdkClass("com.flir.thermalsdk.live.Identity");
732
+ Method connectMethod = cameraClass.getMethod("connect",
733
+ identityClass, connStatusClass, connectParamsClass);
734
+ connectMethod.invoke(cameraObj, deviceInfo.identity, connListener, connectParams);
735
+ connected = true;
736
+ } catch (NoSuchMethodException ignored) {}
737
+
738
+ // Try connect(Identity, ConnectionStatusListener)
739
+ if (!connected) {
740
+ try {
741
+ Class<?> identityClass = findSdkClass("com.flir.thermalsdk.live.Identity");
742
+ Method connectMethod = cameraClass.getMethod("connect",
743
+ identityClass, connStatusClass);
744
+ connectMethod.invoke(cameraObj, deviceInfo.identity, connListener);
745
+ connected = true;
746
+ } catch (NoSuchMethodException ignored) {}
747
+ }
748
+
749
+ // Try connect(Identity)
750
+ if (!connected) {
751
+ try {
752
+ Class<?> identityClass = findSdkClass("com.flir.thermalsdk.live.Identity");
753
+ Method connectMethod = cameraClass.getMethod("connect", identityClass);
754
+ connectMethod.invoke(cameraObj, deviceInfo.identity);
755
+ connected = true;
756
+ } catch (NoSuchMethodException ignored) {}
757
+ }
758
+
759
+ if (!connected) {
760
+ throw new Exception("No suitable connect method found");
761
+ }
762
+
763
+ connectedDevice = deviceInfo;
764
+ isConnected.set(true);
765
+
766
+ logStep("CONNECTED", "Successfully connected to: " + deviceInfo.deviceName);
767
+ Log.i(TAG, "[FLIR] Connected to: " + deviceInfo.deviceName);
768
+
769
+ // Notify listener
770
+ if (listener != null) {
771
+ mainHandler.post(() -> listener.onDeviceConnected(
772
+ deviceInfo.deviceId, deviceInfo.deviceName, deviceInfo.isEmulator));
773
+ }
774
+
775
+ // Start streaming after brief delay
776
+ logStep("SCHEDULE_STREAMING", "Scheduling stream start in 500ms");
777
+ scheduler.schedule(this::startStreaming, 500, TimeUnit.MILLISECONDS);
778
+
779
+ } catch (Throwable t) {
780
+ logStep("CONNECT_FAILED", "Connection failed: " + t.getMessage());
781
+ Log.e(TAG, "[FLIR] Connection failed: " + t.getMessage(), t);
782
+ notifyError("Connection failed: " + t.getMessage());
783
+ }
784
+ });
785
+ }
786
+
787
+ private void handleDisconnected() {
788
+ cameraObj = null;
789
+ streamerObj = null;
790
+ currentStream = null;
791
+ connectedDevice = null;
792
+ isConnected.set(false);
793
+ isStreaming.set(false);
794
+
795
+ if (listener != null) {
796
+ mainHandler.post(() -> listener.onDeviceDisconnected());
797
+ }
798
+ }
799
+
800
+ // ==================== STREAMING IMPLEMENTATION ====================
801
+
802
+ private void startStreaming() {
803
+ if (cameraObj == null) {
804
+ logStep("STREAM_SKIP", "Cannot start streaming - no camera object");
805
+ Log.w(TAG, "[FLIR] Cannot start streaming - no camera");
806
+ return;
807
+ }
808
+
809
+ logStep("STREAM_START", "Starting streaming process...");
810
+ Log.i(TAG, "[FLIR] Starting streaming...");
811
+
812
+ try {
813
+ // Get camera streams
814
+ Method getStreams = cameraObj.getClass().getMethod("getStreams");
815
+ Object streams = getStreams.invoke(cameraObj);
816
+
817
+ if (streams == null || !(streams instanceof List) || ((List<?>) streams).isEmpty()) {
818
+ logStep("NO_STREAMS", "No streams available from camera");
819
+ Log.w(TAG, "[FLIR] No streams available");
820
+ return;
821
+ }
822
+
823
+ List<?> streamList = (List<?>) streams;
824
+ logStep("STREAMS_FOUND", "Found " + streamList.size() + " stream(s)");
825
+ Log.i(TAG, "[FLIR] Found " + streamList.size() + " stream(s)");
826
+
827
+ // Prefer thermal stream, fallback to first available
828
+ Object chosenStream = null;
829
+ String streamType = "unknown";
830
+
831
+ for (Object s : streamList) {
832
+ if (s == null) continue;
833
+
834
+ try {
835
+ Method isThermal = s.getClass().getMethod("isThermal");
836
+ Boolean thermal = (Boolean) isThermal.invoke(s);
837
+
838
+ Method getName = null;
839
+ try { getName = s.getClass().getMethod("getName"); } catch (Throwable ignored) {}
840
+ String name = getName != null ? String.valueOf(getName.invoke(s)) : "unknown";
841
+
842
+ Log.d(TAG, "[FLIR] Stream: " + name + ", thermal=" + thermal);
843
+
844
+ if (thermal != null && thermal) {
845
+ chosenStream = s;
846
+ streamType = "thermal";
847
+ break;
848
+ } else if (chosenStream == null) {
849
+ chosenStream = s;
850
+ streamType = "visual";
851
+ }
852
+ } catch (Throwable ignored) {}
853
+ }
854
+
855
+ if (chosenStream == null) {
856
+ chosenStream = streamList.get(0);
857
+ streamType = "default";
858
+ }
859
+
860
+ currentStream = chosenStream;
861
+ currentStreamKind = streamType;
862
+
863
+ logStep("STREAM_SELECTED", "Using " + streamType + " stream");
864
+ Log.i(TAG, "[FLIR] Using stream type: " + streamType);
865
+
866
+ // Create appropriate streamer
867
+ if ("thermal".equals(streamType)) {
868
+ logStep("CREATE_THERMAL_STREAMER", "Creating ThermalStreamer");
869
+ Class<?> thermalStreamerClass = findSdkClass("com.flir.thermalsdk.live.streaming.ThermalStreamer");
870
+ Class<?> streamClass = findSdkClass("com.flir.thermalsdk.live.streaming.Stream");
871
+ streamerObj = thermalStreamerClass.getConstructor(streamClass).newInstance(chosenStream);
872
+
873
+ // Initialize default palette if not already set
874
+ if (currentPalette == null) {
875
+ logStep("INIT_PALETTE", "Initializing default 'iron' palette");
876
+ initializeDefaultPalette();
877
+ } else {
878
+ logStep("PALETTE_EXISTS", "Palette already set, skipping init");
879
+ }
880
+ } else {
881
+ logStep("CREATE_VISUAL_STREAMER", "Creating VisualStreamer");
882
+ Class<?> visualStreamerClass = findSdkClass("com.flir.thermalsdk.live.streaming.VisualStreamer");
883
+ Class<?> streamClass = findSdkClass("com.flir.thermalsdk.live.streaming.Stream");
884
+ streamerObj = visualStreamerClass.getConstructor(streamClass).newInstance(chosenStream);
885
+ }
886
+
887
+ // Create OnReceived callback
888
+ Class<?> onReceivedClass = findSdkClass("com.flir.thermalsdk.live.remote.OnReceived");
889
+ Object onReceivedCallback = Proxy.newProxyInstance(
890
+ getEffectiveClassLoader(),
891
+ new Class<?>[] { onReceivedClass },
892
+ (proxy, method, args) -> {
893
+ if ("run".equals(method.getName())) {
894
+ scheduler.submit(this::processFrame);
895
+ }
896
+ return null;
897
+ }
898
+ );
899
+
900
+ // Create OnRemoteError callback
901
+ Class<?> onErrorClass = findSdkClass("com.flir.thermalsdk.live.remote.OnRemoteError");
902
+ Object onErrorCallback = Proxy.newProxyInstance(
903
+ getEffectiveClassLoader(),
904
+ new Class<?>[] { onErrorClass },
905
+ (proxy, method, args) -> {
906
+ if ("run".equals(method.getName()) && args != null && args.length > 0) {
907
+ Log.e(TAG, "[FLIR] Stream error: " + args[0]);
908
+ }
909
+ return null;
910
+ }
911
+ );
912
+
913
+ // Start the stream
914
+ logStep("STREAM_STARTING", "Invoking stream.start()");
915
+ Method startMethod = chosenStream.getClass().getMethod("start", onReceivedClass, onErrorClass);
916
+ startMethod.invoke(chosenStream, onReceivedCallback, onErrorCallback);
917
+
918
+ isStreaming.set(true);
919
+
920
+ logStep("STREAM_STARTED", "Stream started successfully - type=" + streamType + ", waiting for frames...");
921
+ Log.i(TAG, "[FLIR] Streaming started (" + streamType + ")");
922
+
923
+ if (listener != null) {
924
+ final String type = streamType;
925
+ mainHandler.post(() -> listener.onStreamStarted(type));
926
+ }
927
+
928
+ } catch (Throwable t) {
929
+ logStep("STREAM_ERROR", "Streaming failed: " + t.getMessage());
930
+ Log.e(TAG, "[FLIR] startStreaming failed: " + t.getMessage(), t);
931
+ notifyError("Streaming failed: " + t.getMessage());
932
+ }
933
+ }
934
+
935
+ private void processFrame() {
936
+ if (streamerObj == null) return;
937
+
938
+ frameCount++;
939
+ long now = System.currentTimeMillis();
940
+
941
+ // Log first frame and then every 30 frames or every 5 seconds
942
+ boolean shouldLog = frameCount == 1 || frameCount % 30 == 0 || (now - lastFrameLogTime > 5000);
943
+
944
+ if (shouldLog) {
945
+ logStep("PROCESS_FRAME", "frame=" + frameCount + ", streamType=" + currentStreamKind + ", bitmapsSuccess=" + successfulBitmapCount);
946
+ lastFrameLogTime = now;
947
+ }
948
+
949
+ try {
950
+ // Call streamer.update() first - this refreshes the streamer content
951
+ Method updateMethod = streamerObj.getClass().getMethod("update");
952
+ updateMethod.invoke(streamerObj);
953
+
954
+ // Get image buffer from streamer.getImage()
955
+ Method getImage = streamerObj.getClass().getMethod("getImage");
956
+ Object imageBuffer = getImage.invoke(streamerObj);
957
+
958
+ if (imageBuffer == null) {
959
+ if (shouldLog) logStep("FRAME_NULL_BUFFER", "imageBuffer is null at frame " + frameCount);
960
+ Log.d(TAG, "[FLIR] imageBuffer is null");
961
+ return;
962
+ }
963
+
964
+ if (frameCount == 1) {
965
+ logStep("FIRST_BUFFER", "Got first imageBuffer, type=" + imageBuffer.getClass().getSimpleName());
966
+ }
967
+
968
+ // For thermal streamer, we MUST create the bitmap INSIDE withThermalImage callback
969
+ // This is critical - the palette affects the imageBuffer rendering only while inside the callback
970
+ if ("thermal".equals(currentStreamKind)) {
971
+ processThermalFrameWithCallback(imageBuffer);
972
+ } else {
973
+ // For visual stream, just convert to bitmap directly
974
+ Bitmap bitmap = convertToBitmap(imageBuffer);
975
+ if (bitmap != null) {
976
+ successfulBitmapCount++;
977
+ latestFrame = bitmap;
978
+ if (frameCount == 1) {
979
+ logStep("FIRST_BITMAP", "Visual bitmap created: " + bitmap.getWidth() + "x" + bitmap.getHeight());
980
+ }
981
+ if (listener != null) {
982
+ listener.onFrame(bitmap);
983
+ }
984
+ } else if (frameCount <= 5) {
985
+ logStep("BITMAP_NULL", "Visual convertToBitmap returned null at frame " + frameCount);
986
+ }
987
+ }
988
+
989
+ } catch (Throwable t) {
990
+ // Log errors periodically to avoid spam
991
+ if (System.currentTimeMillis() % 5000 < 100) {
992
+ Log.d(TAG, "[FLIR] Frame processing error: " + t.getMessage());
993
+ }
994
+ }
995
+ }
996
+
997
+ /**
998
+ * Process thermal frame using withThermalImage callback pattern.
999
+ * CRITICAL: Both palette setting AND bitmap conversion must happen INSIDE the callback!
1000
+ * This matches the LiveStreamingKotlin example exactly.
1001
+ */
1002
+ private void processThermalFrameWithCallback(Object imageBuffer) {
1003
+ if (streamerObj == null) return;
1004
+
1005
+ try {
1006
+ Class<?> thermalStreamerClass = findSdkClass("com.flir.thermalsdk.live.streaming.ThermalStreamer");
1007
+ if (!thermalStreamerClass.isInstance(streamerObj)) {
1008
+ // Not a ThermalStreamer, use direct conversion
1009
+ Bitmap bitmap = convertToBitmap(imageBuffer);
1010
+ if (bitmap != null) {
1011
+ latestFrame = bitmap;
1012
+ if (listener != null) listener.onFrame(bitmap);
1013
+ }
1014
+ return;
1015
+ }
1016
+
1017
+ // Find withThermalImage method
1018
+ Method withThermalImageMethod = null;
1019
+ for (Method m : thermalStreamerClass.getMethods()) {
1020
+ if ("withThermalImage".equals(m.getName()) && m.getParameterCount() == 1) {
1021
+ withThermalImageMethod = m;
1022
+ break;
1023
+ }
1024
+ }
1025
+
1026
+ if (withThermalImageMethod == null) {
1027
+ Log.w(TAG, "[FLIR] withThermalImage method not found, using fallback");
1028
+ // Fallback: try to set palette via getThermalImage, then convert
1029
+ try {
1030
+ Method getThermalImage = streamerObj.getClass().getMethod("getThermalImage");
1031
+ Object thermalImage = getThermalImage.invoke(streamerObj);
1032
+ if (thermalImage != null && currentPalette != null) {
1033
+ setPaletteOnThermalImage(thermalImage);
1034
+ }
1035
+ } catch (Throwable ignored) {}
1036
+
1037
+ Bitmap bitmap = convertToBitmap(imageBuffer);
1038
+ if (bitmap != null) {
1039
+ latestFrame = bitmap;
1040
+ if (listener != null) listener.onFrame(bitmap);
1041
+ }
1042
+ return;
1043
+ }
1044
+
1045
+ // Create Consumer proxy that does BOTH: set palette AND create bitmap
1046
+ // This is the key insight from LiveStreamingKotlin!
1047
+ final Object imgBuffer = imageBuffer;
1048
+ Class<?> consumerClass = withThermalImageMethod.getParameterTypes()[0];
1049
+ Object consumer = Proxy.newProxyInstance(
1050
+ getEffectiveClassLoader(),
1051
+ new Class<?>[] { consumerClass },
1052
+ (proxy, method, args) -> {
1053
+ if ("accept".equals(method.getName()) && args != null && args.length > 0) {
1054
+ Object thermalImage = args[0];
1055
+ if (thermalImage != null) {
1056
+ // Step 1: Set palette on thermal image
1057
+ if (currentPalette != null) {
1058
+ setPaletteOnThermalImage(thermalImage);
1059
+ if (frameCount == 1) {
1060
+ logStep("PALETTE_APPLIED", "Palette applied to ThermalImage inside callback");
1061
+ }
1062
+ }
1063
+
1064
+ // Step 2: Convert to bitmap INSIDE the callback
1065
+ // This is critical - must happen while we have access to thermal image context
1066
+ try {
1067
+ Bitmap bitmap = convertToBitmap(imgBuffer);
1068
+ if (bitmap != null) {
1069
+ successfulBitmapCount++;
1070
+ latestFrame = bitmap;
1071
+ if (frameCount == 1) {
1072
+ logStep("FIRST_THERMAL_BITMAP", "Thermal bitmap created INSIDE callback: " + bitmap.getWidth() + "x" + bitmap.getHeight());
1073
+ }
1074
+ if (listener != null) {
1075
+ listener.onFrame(bitmap);
1076
+ }
1077
+ } else if (frameCount <= 5) {
1078
+ logStep("THERMAL_BITMAP_NULL", "Thermal convertToBitmap returned null at frame " + frameCount);
1079
+ }
1080
+ } catch (Throwable t) {
1081
+ if (frameCount <= 5) {
1082
+ logStep("THERMAL_BITMAP_ERROR", "Bitmap conversion failed: " + t.getMessage());
1083
+ }
1084
+ Log.d(TAG, "[FLIR] Bitmap conversion in callback failed: " + t.getMessage());
1085
+ }
1086
+ } else if (frameCount <= 5) {
1087
+ logStep("THERMAL_IMAGE_NULL", "ThermalImage is null in callback");
1088
+ }
1089
+ }
1090
+ return null;
1091
+ }
1092
+ );
1093
+
1094
+ withThermalImageMethod.invoke(streamerObj, consumer);
1095
+
1096
+ } catch (Throwable t) {
1097
+ Log.d(TAG, "[FLIR] processThermalFrameWithCallback failed: " + t.getMessage());
1098
+ // Fallback to direct conversion
1099
+ Bitmap bitmap = convertToBitmap(imageBuffer);
1100
+ if (bitmap != null) {
1101
+ latestFrame = bitmap;
1102
+ if (listener != null) listener.onFrame(bitmap);
1103
+ }
1104
+ }
1105
+ }
1106
+
1107
+ /**
1108
+ * Apply palette via ThermalStreamer.withThermalImage() callback pattern.
1109
+ * In the SDK: thermalStreamer.withThermalImage { it.palette = selectedPalette }
1110
+ * NOTE: This is now mostly unused - processThermalFrameWithCallback handles both palette and bitmap
1111
+ */
1112
+ private void applyPaletteViaThermalImage() {
1113
+ if (streamerObj == null || currentPalette == null) return;
1114
+
1115
+ try {
1116
+ Class<?> thermalStreamerClass = findSdkClass("com.flir.thermalsdk.live.streaming.ThermalStreamer");
1117
+ if (!thermalStreamerClass.isInstance(streamerObj)) return;
1118
+
1119
+ // Find withThermalImage method - it takes a Consumer<ThermalImage> callback
1120
+ // We'll use reflection to create a proxy for the Consumer interface
1121
+ Method withThermalImageMethod = null;
1122
+ for (Method m : thermalStreamerClass.getMethods()) {
1123
+ if ("withThermalImage".equals(m.getName()) && m.getParameterCount() == 1) {
1124
+ withThermalImageMethod = m;
1125
+ break;
1126
+ }
1127
+ }
1128
+
1129
+ if (withThermalImageMethod == null) {
1130
+ // Fallback: Try to get ThermalImage directly
1131
+ try {
1132
+ Method getThermalImage = streamerObj.getClass().getMethod("getThermalImage");
1133
+ Object thermalImage = getThermalImage.invoke(streamerObj);
1134
+ if (thermalImage != null) {
1135
+ setPaletteOnThermalImage(thermalImage);
1136
+ }
1137
+ } catch (Throwable ignored) {}
1138
+ return;
1139
+ }
1140
+
1141
+ // Create Consumer proxy to call setPalette on ThermalImage
1142
+ Class<?> consumerClass = withThermalImageMethod.getParameterTypes()[0];
1143
+ Object consumer = Proxy.newProxyInstance(
1144
+ getEffectiveClassLoader(),
1145
+ new Class<?>[] { consumerClass },
1146
+ (proxy, method, args) -> {
1147
+ if ("accept".equals(method.getName()) && args != null && args.length > 0) {
1148
+ Object thermalImage = args[0];
1149
+ if (thermalImage != null) {
1150
+ setPaletteOnThermalImage(thermalImage);
1151
+ }
1152
+ }
1153
+ return null;
1154
+ }
1155
+ );
1156
+
1157
+ withThermalImageMethod.invoke(streamerObj, consumer);
1158
+
1159
+ } catch (Throwable t) {
1160
+ Log.d(TAG, "[FLIR] applyPaletteViaThermalImage failed: " + t.getMessage());
1161
+ }
1162
+ }
1163
+
1164
+ /**
1165
+ * Set palette on a ThermalImage object
1166
+ */
1167
+ private void setPaletteOnThermalImage(Object thermalImage) {
1168
+ if (thermalImage == null || currentPalette == null) return;
1169
+
1170
+ try {
1171
+ Class<?> paletteClass = findSdkClass("com.flir.thermalsdk.image.Palette");
1172
+ Method setPalette = thermalImage.getClass().getMethod("setPalette", paletteClass);
1173
+ setPalette.invoke(thermalImage, currentPalette);
1174
+ } catch (Throwable t) {
1175
+ // Try with direct class
1176
+ try {
1177
+ Method setPalette = thermalImage.getClass().getMethod("setPalette", currentPalette.getClass());
1178
+ setPalette.invoke(thermalImage, currentPalette);
1179
+ } catch (Throwable ignored) {}
1180
+ }
1181
+ }
1182
+
1183
+ private Bitmap convertToBitmap(Object imageBuffer) {
1184
+ boolean isFirstConvert = frameCount == 1;
1185
+
1186
+ try {
1187
+ // Try BitmapAndroid.createBitmap(imageBuffer).bitMap (or getBitMap)
1188
+ Class<?> bitmapAndroidClass = findSdkClass("com.flir.thermalsdk.androidsdk.image.BitmapAndroid");
1189
+
1190
+ if (isFirstConvert) {
1191
+ logStep("BITMAP_CONVERT_START", "imageBuffer type=" + imageBuffer.getClass().getName());
1192
+ }
1193
+
1194
+ // Find a createBitmap method that works with our imageBuffer
1195
+ for (Method m : bitmapAndroidClass.getMethods()) {
1196
+ if ("createBitmap".equals(m.getName()) && m.getParameterCount() == 1) {
1197
+ Class<?> paramType = m.getParameterTypes()[0];
1198
+ if (paramType.isInstance(imageBuffer)) {
1199
+ Object wrapper = m.invoke(null, imageBuffer);
1200
+ if (wrapper != null) {
1201
+ if (isFirstConvert) {
1202
+ logStep("BITMAP_WRAPPER", "BitmapAndroid wrapper created: " + wrapper.getClass().getSimpleName());
1203
+ }
1204
+
1205
+ // Try different method names: bitMap, getBitMap, getBitmap
1206
+ String[] methodNames = {"getBitMap", "bitMap", "getBitmap"};
1207
+ for (String methodName : methodNames) {
1208
+ try {
1209
+ Method getBitMap = wrapper.getClass().getMethod(methodName);
1210
+ Object bmp = getBitMap.invoke(wrapper);
1211
+ if (bmp instanceof Bitmap) {
1212
+ Bitmap bitmap = (Bitmap) bmp;
1213
+ if (bitmap.getWidth() > 0 && bitmap.getHeight() > 0) {
1214
+ if (isFirstConvert) {
1215
+ logStep("BITMAP_SUCCESS", "Got bitmap via " + methodName + "(): " + bitmap.getWidth() + "x" + bitmap.getHeight());
1216
+ }
1217
+ return bitmap;
1218
+ } else if (isFirstConvert) {
1219
+ logStep("BITMAP_EMPTY", "Bitmap has zero dimensions via " + methodName);
1220
+ }
1221
+ }
1222
+ } catch (NoSuchMethodException ignored) {}
1223
+ }
1224
+
1225
+ // Also try as a field access (Kotlin property)
1226
+ try {
1227
+ java.lang.reflect.Field field = wrapper.getClass().getField("bitMap");
1228
+ Object bmp = field.get(wrapper);
1229
+ if (bmp instanceof Bitmap) {
1230
+ return (Bitmap) bmp;
1231
+ }
1232
+ } catch (Throwable ignored) {}
1233
+ }
1234
+ }
1235
+ }
1236
+ }
1237
+
1238
+ Log.d(TAG, "[FLIR] BitmapAndroid.createBitmap method not found for imageBuffer type: " + imageBuffer.getClass().getName());
1239
+
1240
+ } catch (Throwable t) {
1241
+ Log.d(TAG, "[FLIR] convertToBitmap primary method failed: " + t.getMessage());
1242
+ }
1243
+
1244
+ // Try alternative: direct getBitmap() on imageBuffer
1245
+ try {
1246
+ Method getBitmap = imageBuffer.getClass().getMethod("getBitmap");
1247
+ Object bmp = getBitmap.invoke(imageBuffer);
1248
+ if (bmp instanceof Bitmap) {
1249
+ return (Bitmap) bmp;
1250
+ }
1251
+ } catch (Throwable ignored) {}
1252
+
1253
+ return null;
1254
+ }
1255
+
1256
+ private void stopStreaming() {
1257
+ if (currentStream != null) {
1258
+ try {
1259
+ Method stopMethod = currentStream.getClass().getMethod("stop");
1260
+ stopMethod.invoke(currentStream);
1261
+ Log.i(TAG, "[FLIR] Stream stopped");
1262
+ } catch (Throwable t) {
1263
+ Log.w(TAG, "[FLIR] stopStreaming failed: " + t.getMessage());
1264
+ }
1265
+ }
1266
+
1267
+ streamerObj = null;
1268
+ currentStream = null;
1269
+ isStreaming.set(false);
1270
+ }
1271
+
1272
+ // ==================== SDK LOADING ====================
1273
+
1274
+ private boolean initializeSdk() {
1275
+ // Try direct class loading first
1276
+ try {
1277
+ Class.forName("com.flir.thermalsdk.live.CommunicationInterface");
1278
+ Log.i(TAG, "[FLIR SDK] Classes available on classpath");
1279
+
1280
+ // Initialize SDK
1281
+ initializeThermalSdk();
1282
+ return true;
1283
+ } catch (ClassNotFoundException e) {
1284
+ Log.w(TAG, "[FLIR SDK] Classes not on classpath, trying AAR load");
1285
+ }
1286
+
1287
+ // Try loading from downloaded AAR
1288
+ if (attemptLoadSdkFromAar()) {
1289
+ initializeThermalSdk();
1290
+ return true;
1291
+ }
1292
+
1293
+ return false;
1294
+ }
1295
+
1296
+ private void initializeThermalSdk() {
1297
+ try {
1298
+ Class<?> sdkClass = findSdkClass("com.flir.thermalsdk.live.ThermalSdkAndroid");
1299
+ Method initMethod = sdkClass.getMethod("init", android.content.Context.class);
1300
+ initMethod.invoke(null, appContext);
1301
+ Log.i(TAG, "[FLIR SDK] ThermalSdkAndroid.init() completed");
1302
+ } catch (Throwable t) {
1303
+ Log.w(TAG, "[FLIR SDK] ThermalSdkAndroid.init() failed: " + t.getMessage());
1304
+ }
1305
+ }
1306
+
1307
+ private boolean attemptLoadSdkFromAar() {
1308
+ android.content.Context ctx = appContext;
1309
+ if (ctx == null) {
1310
+ Log.w(TAG, "[FLIR SDK] No application context available for SDK load");
1311
+ return false;
1312
+ }
1313
+
1314
+ try {
1315
+ // First try: Load from architecture-specific DEX (new format from FlirSDKLoader)
1316
+ File dexFile = FlirSDKLoader.INSTANCE.getDexPath(ctx);
1317
+ if (dexFile != null && dexFile.exists()) {
1318
+ Log.i(TAG, "[FLIR SDK] Found DEX file: " + dexFile.getAbsolutePath() + " (size=" + dexFile.length() + ")");
1319
+
1320
+ // Get native library directory path for DexClassLoader
1321
+ File nativeLibDir = FlirSDKLoader.INSTANCE.getNativeLibDir(ctx);
1322
+ String nativeLibPath = nativeLibDir != null ? nativeLibDir.getAbsolutePath() : null;
1323
+ Log.i(TAG, "[FLIR SDK] Native lib path: " + nativeLibPath);
1324
+
1325
+ // Create DexClassLoader with native lib path
1326
+ File dexOutDir = ctx.getDir("dex", android.content.Context.MODE_PRIVATE);
1327
+ DexClassLoader dcl = new DexClassLoader(
1328
+ dexFile.getAbsolutePath(),
1329
+ dexOutDir.getAbsolutePath(),
1330
+ nativeLibPath, // This allows SDK to find .so files
1331
+ ctx.getClassLoader()
1332
+ );
1333
+
1334
+ // Verify class loading
1335
+ Class<?> test = Class.forName("com.flir.thermalsdk.live.CommunicationInterface", true, dcl);
1336
+ if (test != null) {
1337
+ sdkClassLoader = dcl;
1338
+ sdkJarPath = dexFile.getAbsolutePath();
1339
+ Log.i(TAG, "[FLIR SDK] DexClassLoader created from DEX: " + dexFile.getAbsolutePath());
1340
+ return true;
1341
+ }
1342
+ }
1343
+
1344
+ // Fallback: Legacy AAR loading
1345
+ Log.i(TAG, "[FLIR SDK] No DEX found, trying legacy AAR locations...");
1346
+
1347
+ File filesDir = ctx.getFilesDir();
1348
+
1349
+ // Candidate search locations (ordered by preference)
1350
+ List<File> candidates = new ArrayList<>();
1351
+
1352
+ // Primary: FlirSDKLoader download directory
1353
+ candidates.add(new File(filesDir, "FlirSDK/thermalsdk-release.aar"));
1354
+ candidates.add(new File(filesDir, "FlirSDK/androidsdk-release.aar"));
1355
+ candidates.add(new File(filesDir, "FlirSDK/thermalsdk.aar"));
1356
+
1357
+ // Legacy locations
1358
+ candidates.add(new File(filesDir, "flir-sdk/thermalsdk-release.aar"));
1359
+ candidates.add(new File(filesDir, "thermalsdk-release.aar"));
1360
+ candidates.add(new File(filesDir, "thermalsdk.aar"));
1361
+
1362
+ // External storage
1363
+ File extDir = ctx.getExternalFilesDir(null);
1364
+ if (extDir != null) {
1365
+ candidates.add(new File(extDir, "FlirSDK/thermalsdk-release.aar"));
1366
+ candidates.add(new File(extDir, "thermalsdk-release.aar"));
1367
+ }
1368
+
1369
+ // Find first existing AAR
1370
+ File aarFile = null;
1371
+ StringBuilder tried = new StringBuilder();
1372
+ for (File f : candidates) {
1373
+ tried.append(f.getAbsolutePath()).append(f.exists() ? "(✓)," : "(✗),");
1374
+ if (f.exists()) {
1375
+ aarFile = f;
1376
+ break;
1377
+ }
1378
+ }
1379
+
1380
+ if (aarFile == null) {
1381
+ Log.w(TAG, "[FLIR SDK] No AAR found. Tried: " + tried);
1382
+ return false;
1383
+ }
1384
+
1385
+ Log.i(TAG, "[FLIR SDK] Found AAR: " + aarFile.getAbsolutePath());
1386
+
1387
+ // Extract classes.jar from AAR to a private directory
1388
+ ZipFile zf = new ZipFile(aarFile);
1389
+ ZipEntry classesEntry = zf.getEntry("classes.jar");
1390
+ if (classesEntry == null) {
1391
+ Log.w(TAG, "[FLIR SDK] classes.jar not found in AAR");
1392
+ zf.close();
1393
+ return false;
1394
+ }
1395
+
1396
+ // Use getDir() for a MODE_PRIVATE directory - required for DexClassLoader security
1397
+ File privateDir = ctx.getDir("flir_sdk", android.content.Context.MODE_PRIVATE);
1398
+ File outJar = new File(privateDir, "flir-classes.jar");
1399
+
1400
+ // Delete old file if exists to ensure clean extraction
1401
+ if (outJar.exists()) {
1402
+ outJar.delete();
1403
+ }
1404
+
1405
+ FileOutputStream fos = new FileOutputStream(outJar);
1406
+ java.io.InputStream is = zf.getInputStream(classesEntry);
1407
+ byte[] buf = new byte[8192];
1408
+ int r;
1409
+ while ((r = is.read(buf)) != -1) fos.write(buf, 0, r);
1410
+ is.close();
1411
+ fos.close();
1412
+ zf.close();
1413
+
1414
+ // Set file to read-only (required for Android security)
1415
+ outJar.setReadOnly();
1416
+
1417
+ Log.i(TAG, "[FLIR SDK] Extracted classes.jar to: " + outJar.getAbsolutePath() + " (size=" + outJar.length() + ")");
1418
+
1419
+ // Create DexClassLoader with private dex output directory
1420
+ File dexOutDir = ctx.getDir("dex", android.content.Context.MODE_PRIVATE);
1421
+ DexClassLoader dcl = new DexClassLoader(
1422
+ outJar.getAbsolutePath(),
1423
+ dexOutDir.getAbsolutePath(),
1424
+ null,
1425
+ ctx.getClassLoader()
1426
+ );
1427
+
1428
+ // Verify class loading
1429
+ Class<?> test = Class.forName("com.flir.thermalsdk.live.CommunicationInterface", true, dcl);
1430
+ if (test != null) {
1431
+ sdkClassLoader = dcl;
1432
+ sdkJarPath = outJar.getAbsolutePath();
1433
+ Log.i(TAG, "[FLIR SDK] DexClassLoader created from: " + outJar.getAbsolutePath());
1434
+ return true;
1435
+ }
1436
+
1437
+ } catch (Throwable t) {
1438
+ Log.e(TAG, "[FLIR SDK] attemptLoadSdkFromAar failed: " + t.getMessage(), t);
1439
+ }
1440
+
1441
+ return false;
1442
+ }
1443
+
1444
+ private Class<?> findSdkClass(String name) throws ClassNotFoundException {
1445
+ try {
1446
+ return Class.forName(name);
1447
+ } catch (ClassNotFoundException e) {
1448
+ if (sdkClassLoader != null) {
1449
+ return Class.forName(name, true, sdkClassLoader);
1450
+ }
1451
+ throw e;
1452
+ }
1453
+ }
1454
+
1455
+ private ClassLoader getEffectiveClassLoader() {
1456
+ return sdkClassLoader != null ? sdkClassLoader : getClass().getClassLoader();
1457
+ }
1458
+
1459
+ // ==================== HELPERS ====================
1460
+
1461
+ private String extractDeviceId(Object identity) {
1462
+ try {
1463
+ Method getDeviceId = identity.getClass().getMethod("getDeviceId");
1464
+ Object result = getDeviceId.invoke(identity);
1465
+ return result != null ? result.toString() : "unknown";
1466
+ } catch (Throwable t) {
1467
+ return "device_" + System.currentTimeMillis();
1468
+ }
1469
+ }
1470
+
1471
+ private String extractDeviceName(Object identity) {
1472
+ try {
1473
+ // Try getName() first
1474
+ try {
1475
+ Method getName = identity.getClass().getMethod("getName");
1476
+ Object result = getName.invoke(identity);
1477
+ if (result != null && !result.toString().isEmpty()) {
1478
+ return result.toString();
1479
+ }
1480
+ } catch (Throwable ignored) {}
1481
+
1482
+ // Try getDeviceId() as fallback
1483
+ Method getDeviceId = identity.getClass().getMethod("getDeviceId");
1484
+ Object result = getDeviceId.invoke(identity);
1485
+ return result != null ? result.toString() : "FLIR Camera";
1486
+ } catch (Throwable t) {
1487
+ return "FLIR Camera";
1488
+ }
1489
+ }
1490
+
1491
+ private CommInterface extractCommInterface(Object identity) {
1492
+ try {
1493
+ Method getCommInterface = identity.getClass().getMethod("getCommunicationInterface");
1494
+ Object result = getCommInterface.invoke(identity);
1495
+ if (result != null) {
1496
+ String name = result.toString();
1497
+ if (name.contains("USB")) return CommInterface.USB;
1498
+ if (name.contains("NETWORK")) return CommInterface.NETWORK;
1499
+ if (name.contains("EMULATOR")) return CommInterface.EMULATOR;
1500
+ }
1501
+ } catch (Throwable ignored) {}
1502
+ return CommInterface.EMULATOR;
1503
+ }
1504
+
1505
+ private void notifyError(String error) {
1506
+ Log.e(TAG, "[FLIR] Error: " + error);
1507
+ if (listener != null) {
1508
+ mainHandler.post(() -> listener.onError(error));
1509
+ }
1510
+ }
1511
+ }