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,890 +1,1511 @@
1
1
  package flir.android;
2
2
 
3
3
  import android.graphics.Bitmap;
4
+ import android.os.Handler;
5
+ import android.os.Looper;
4
6
  import android.util.Log;
5
7
 
8
+ import java.io.File;
9
+ import java.io.FileOutputStream;
6
10
  import java.lang.reflect.Method;
7
11
  import java.lang.reflect.Proxy;
12
+ import java.util.ArrayList;
8
13
  import java.util.List;
9
- import java.io.File;
10
- import java.io.FileOutputStream;
11
- import java.util.zip.ZipEntry;
12
- import java.util.zip.ZipFile;
13
- import dalvik.system.DexClassLoader;
14
+ import java.util.concurrent.CopyOnWriteArrayList;
14
15
  import java.util.concurrent.Executors;
15
16
  import java.util.concurrent.ScheduledExecutorService;
17
+ import java.util.concurrent.ScheduledFuture;
16
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;
17
24
 
18
25
  /**
19
- * Thin helper that wraps FLIR Atlas SDK emulator and streamer features.
20
- * All SDK-specific reflection plumbing lives here so FlirManager stays small.
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)
21
45
  */
22
- class FlirSdkManager {
46
+ public class FlirSdkManager {
23
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
+ }
24
83
 
25
- interface Listener {
84
+ // Listener interface for callbacks
85
+ public interface Listener {
26
86
  void onFrame(Bitmap bitmap);
27
-
28
87
  void onTemperature(double temp, int x, int y);
29
-
30
- void onDeviceFound(String name);
31
-
32
- void onEmulatorEnabled();
33
-
34
- void onStreamKindChanged(String kind);
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
+ }
35
113
  }
36
114
 
37
- private Listener listener;
38
-
39
- // SDK objects
40
- private Object discoveryFactory;
41
- private Object cameraObj;
42
- private Object streamerObj;
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)
43
121
  private ClassLoader sdkClassLoader = null;
44
- private String sdkCurrentStreamKind = null;
45
-
46
- private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
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
47
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;
48
148
 
49
- FlirSdkManager(Listener listener) {
149
+ FlirSdkManager(Listener listener, android.content.Context context) {
50
150
  this.listener = listener;
151
+ this.appContext = context != null ? context.getApplicationContext() : null;
51
152
  }
52
-
53
- boolean startSdkEmulator() {
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
+
54
205
  try {
55
- Class<?> commIfaceClass = findSdkClass("com.flir.thermalsdk.live.CommunicationInterface");
56
- Object emulatorInterface = Enum.valueOf((Class<Enum>) commIfaceClass, "EMULATOR");
57
- Log.d(TAG, "[FLIR SDK] Found EMULATOR interface: " + emulatorInterface);
58
-
59
- Class<?> discoveryFactoryClass = findSdkClass("com.flir.thermalsdk.live.discovery.DiscoveryFactory");
60
- Method getInstance = discoveryFactoryClass.getMethod("getInstance");
61
- discoveryFactory = getInstance.invoke(null);
62
-
63
- Class<?> listenerClass = findSdkClass("com.flir.thermalsdk.live.discovery.DiscoveryEventListener");
64
- Object discoveryListener = Proxy.newProxyInstance(
65
- listenerClass.getClassLoader(),
66
- new Class<?>[] { listenerClass },
67
- (proxy, method, args) -> {
68
- String methodName = method.getName();
69
- switch (methodName) {
70
- case "onCameraFound":
71
- Object discovered = args[0];
72
- Log.i(TAG, "[FLIR SDK] Camera found: " + discovered);
73
- connectToSdkCamera(discovered);
74
- return null;
75
-
76
- case "onCameraLost":
77
- Log.i(TAG, "[FLIR SDK] Camera lost");
78
- cameraObj = null;
79
- streamerObj = null;
80
- return null;
81
- default:
82
- return null;
83
- }
84
- });
85
-
86
- // Get NETWORK interface for FLIR ONE Edge
87
- Object networkInterface = Enum.valueOf((Class<Enum>) commIfaceClass, "NETWORK");
88
-
89
- Method scanMethod = discoveryFactoryClass.getMethod("scan", listenerClass,
90
- java.lang.reflect.Array.newInstance(commIfaceClass, 0).getClass());
91
- // Scan for BOTH NETWORK (FLIR ONE Edge) and EMULATOR
92
- Object ifaceArray = java.lang.reflect.Array.newInstance(commIfaceClass, 2);
93
- java.lang.reflect.Array.set(ifaceArray, 0, networkInterface);
94
- java.lang.reflect.Array.set(ifaceArray, 1, emulatorInterface);
95
- scanMethod.invoke(discoveryFactory, discoveryListener, ifaceArray);
96
- Log.i(TAG, "[FLIR SDK] Started discovery scan for NETWORK and EMULATOR");
97
- return true;
98
- } catch (ClassNotFoundException cnf) {
99
- Log.w(TAG, "[FLIR SDK] Classes not found: " + cnf.getMessage());
100
- // Try to load classes from an AAR (on-device) via DexClassLoader
101
- try {
102
- if (attemptLoadSdkFromAar()) {
103
- // If class is now available via sdkClassLoader retry using that loader
104
- Log.i(TAG,
105
- "[FLIR SDK] SDK classes found via DexClassLoader, retrying discovery using sdkClassLoader");
106
- try {
107
- return startSdkEmulatorWithClassLoader(sdkClassLoader);
108
- } catch (Throwable t) {
109
- Log.w(TAG, "[FLIR SDK] retry using sdkClassLoader failed: " + t.getMessage());
110
- }
111
- }
112
- } catch (Throwable t) {
113
- Log.w(TAG, "[FLIR SDK] DexClassLoader attempt failed: " + t.getMessage());
206
+ if (discoveryFactory != null) {
207
+ Method stopMethod = discoveryFactory.getClass().getMethod("stop");
208
+ stopMethod.invoke(discoveryFactory);
114
209
  }
115
- return false;
116
210
  } catch (Throwable t) {
117
- Log.w(TAG, "[FLIR SDK] startSdkEmulator failed: " + t.getMessage(), t);
118
- return false;
211
+ Log.w(TAG, "[FLIR] stopDiscovery failed: " + t.getMessage());
119
212
  }
120
213
  }
121
-
122
- private String sdkJarPath = null;
123
-
124
- private boolean attemptLoadSdkFromAar() {
125
- // TODO: This is legacy code - context should be passed as parameter
126
- android.content.Context ctx = null; // ilabs.libs.io.data.Var.getAppContext();
127
- try {
128
- // android.content.Context ctx = ilabs.libs.io.data.Var.getAppContext();
129
- if (ctx == null) {
130
- Log.w(TAG, "[FLIR SDK] No application context available for SDK load");
131
- return false;
132
- }
133
- File filesDir = ctx.getFilesDir();
134
- // Candidate search locations (ordered by preference)
135
- java.util.List<File> candList = new java.util.ArrayList<>();
136
- // Add internal app filesDir candidates
137
- try {
138
- candList.add(new File(filesDir, "flir-sdk/thermalsdk-release.aar"));
139
- } catch (Throwable ignored) {
140
- }
141
- try {
142
- candList.add(new File(filesDir, "thermalsdk-release.aar"));
143
- } catch (Throwable ignored) {
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;
144
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) {
145
255
  try {
146
- candList.add(new File(filesDir, "thermalsdk.aar"));
147
- } catch (Throwable ignored) {
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());
148
261
  }
149
- // Add external files dir candidates if available
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(() -> {
150
288
  try {
151
- if (ctx.getExternalFilesDir(null) != null) {
152
- candList.add(new File(ctx.getExternalFilesDir(null), "thermalsdk-release.aar"));
153
- candList.add(new File(ctx.getExternalFilesDir(null), "thermalsdk.aar"));
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
+ }
154
305
  }
155
- } catch (Throwable ignored) {
156
- }
157
- // Add common sdcard and tmp locations
158
- try {
159
- candList.add(new File("/sdcard/Download/thermalsdk-release.aar"));
160
- } catch (Throwable ignored) {
161
- }
162
- try {
163
- candList.add(new File("/sdcard/thermalsdk-release.aar"));
164
- } catch (Throwable ignored) {
165
- }
166
- try {
167
- candList.add(new File("/sdcard/thermalsdk.aar"));
168
- } catch (Throwable ignored) {
169
- }
170
- try {
171
- candList.add(new File("/data/local/tmp/thermalsdk-release.aar"));
172
- } catch (Throwable ignored) {
173
- }
174
- try {
175
- candList.add(new File("/data/local/tmp/thermalsdk.aar"));
176
- } catch (Throwable ignored) {
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());
177
315
  }
178
- File candidate = null;
179
- StringBuilder tried = new StringBuilder();
180
- Log.i(TAG,
181
- "[FLIR SDK] FilesDir=" + filesDir.getAbsolutePath() + ", ExternalFilesDir="
182
- + (ctx.getExternalFilesDir(null) != null ? ctx.getExternalFilesDir(null).getAbsolutePath()
183
- : "null"));
184
- for (File cand : candList) {
185
- try {
186
- if (cand == null) {
187
- tried.append("null,");
188
- continue;
189
- }
190
- boolean exists = false;
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
+
191
337
  try {
192
- exists = cand.exists();
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);
193
354
  } catch (Throwable ignored) {
355
+ Log.i(TAG, "[FLIR] Default palette initialized");
194
356
  }
195
- tried.append(cand.getAbsolutePath()).append(exists ? "(exists)," : "(no),");
196
- if (exists) {
197
- candidate = cand;
198
- break;
199
- }
200
- } catch (Throwable ignored) {
201
- tried.append("err,");
202
357
  }
203
358
  }
204
- if (candidate == null || !candidate.exists()) {
205
- Log.w(TAG, "[FLIR SDK] No SDK AAR found. Tried: " + tried.toString());
206
- return false;
207
- }
208
- Log.i(TAG, "[FLIR SDK] Found SDK AAR: " + candidate.getAbsolutePath());
209
- Log.i(TAG, "[FLIR SDK] Candidate list tried: " + tried.toString());
210
- ZipFile zf = new ZipFile(candidate);
211
- ZipEntry classesEntry = zf.getEntry("classes.jar");
212
- if (classesEntry == null) {
213
- Log.w(TAG, "[FLIR SDK] classes.jar not found in AAR");
214
- zf.close();
215
- return false;
216
- }
217
- File outJar = new File(filesDir, "flir-classes.jar");
218
- FileOutputStream fos = new FileOutputStream(outJar);
219
- java.io.InputStream is = zf.getInputStream(classesEntry);
220
- byte[] buf = new byte[8192];
221
- int r;
222
- while ((r = is.read(buf)) != -1)
223
- fos.write(buf, 0, r);
224
- is.close();
225
- fos.close();
226
- zf.close();
227
- File dexOutDir = ctx.getDir("dex", android.content.Context.MODE_PRIVATE);
228
- DexClassLoader dcl = new DexClassLoader(outJar.getAbsolutePath(), dexOutDir.getAbsolutePath(), null,
229
- ctx.getClassLoader());
230
- // verify class present
231
- Class<?> test = Class.forName("com.flir.thermalsdk.live.CommunicationInterface", true, dcl);
232
- if (test != null) {
233
- sdkClassLoader = dcl;
234
- Log.i(TAG, "[FLIR SDK] DexClassLoader created and class loaded from: " + outJar.getAbsolutePath());
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)
235
377
  try {
236
- Log.i(TAG, "[FLIR SDK] DexClassLoader parent loader=" + dcl.getParent() + " (loader=" + dcl + ")");
237
- } catch (Throwable ignored) {
238
- }
239
- sdkJarPath = outJar.getAbsolutePath();
240
- return true;
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) {}
241
395
  }
242
396
  } catch (Throwable t) {
243
- Log.w(TAG, "[FLIR SDK] attemptLoadSdkFromAar failed: " + t.getMessage(), t);
244
- try {
245
- Log.i(TAG, "[FLIR SDK] Candidate list tried on failure: ");
246
- } catch (Throwable ignored) {
247
- }
397
+ Log.d(TAG, "[FLIR] getTemperatureAtPoint failed: " + t.getMessage());
248
398
  }
249
- return false;
399
+
400
+ return Double.NaN;
250
401
  }
251
-
252
- public boolean isSdkLoaded() {
253
- return sdkClassLoader != null;
402
+
403
+ /**
404
+ * Get latest frame bitmap
405
+ */
406
+ public Bitmap getLatestFrame() {
407
+ return latestFrame;
254
408
  }
255
-
256
- public String getLoadedSdkJarPath() {
257
- return sdkJarPath;
409
+
410
+ /**
411
+ * Check if streaming is active
412
+ */
413
+ public boolean isStreamingActive() {
414
+ return isStreaming.get();
258
415
  }
259
-
260
- private Class<?> findSdkClass(String name) throws ClassNotFoundException {
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
+
261
464
  try {
262
- return Class.forName(name);
263
- } catch (ClassNotFoundException e) {
264
- if (sdkClassLoader != null)
265
- return Class.forName(name, true, sdkClassLoader);
266
- throw e;
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();
267
504
  }
268
505
  }
269
-
270
- private boolean startSdkEmulatorWithClassLoader(ClassLoader loader) {
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
+
271
521
  try {
272
- Class<?> commIfaceClass = Class.forName("com.flir.thermalsdk.live.CommunicationInterface", true, loader);
273
- Object emulatorInterface = Enum.valueOf((Class<Enum>) commIfaceClass, "EMULATOR");
274
- Log.d(TAG, "[FLIR SDK] (DEX) CommunicationInterface found via loader: " + loader);
275
- Log.d(TAG, "[FLIR SDK] Found EMULATOR interface (DEX): " + emulatorInterface);
276
-
277
- Class<?> discoveryFactoryClass = Class.forName("com.flir.thermalsdk.live.discovery.DiscoveryFactory", true,
278
- loader);
522
+ // Get DiscoveryFactory
523
+ Class<?> discoveryFactoryClass = findSdkClass("com.flir.thermalsdk.live.discovery.DiscoveryFactory");
279
524
  Method getInstance = discoveryFactoryClass.getMethod("getInstance");
280
525
  discoveryFactory = getInstance.invoke(null);
281
- Log.d(TAG, "[FLIR SDK] (DEX) DiscoveryFactory.getInstance() returned: " + (discoveryFactory != null));
282
-
283
- Class<?> listenerClass = Class.forName("com.flir.thermalsdk.live.discovery.DiscoveryEventListener", true,
284
- loader);
285
- Object discoveryListener = Proxy.newProxyInstance(
286
- loader,
287
- new Class<?>[] { listenerClass },
288
- (proxy, method, args) -> {
289
- String methodName = method.getName();
290
- switch (methodName) {
291
- case "onCameraFound":
292
- Object discovered = args[0];
293
- Log.i(TAG, "[FLIR SDK] (DEX) Camera found: " + discovered);
294
- connectToSdkCamera(discovered);
295
- return null;
296
- case "onCameraLost":
297
- Log.i(TAG, "[FLIR SDK] (DEX) Camera lost");
298
- cameraObj = null;
299
- streamerObj = null;
300
- return null;
301
- default:
302
- return null;
303
- }
304
- });
305
-
306
- // Get NETWORK interface for FLIR ONE Edge
307
- Object networkInterface = Enum.valueOf((Class<Enum>) commIfaceClass, "NETWORK");
308
-
309
- Method scanMethod = discoveryFactoryClass.getMethod("scan", listenerClass,
310
- java.lang.reflect.Array.newInstance(commIfaceClass, 0).getClass());
311
- // Scan for BOTH NETWORK (FLIR ONE Edge) and EMULATOR
312
- Object ifaceArray = java.lang.reflect.Array.newInstance(commIfaceClass, 2);
313
- java.lang.reflect.Array.set(ifaceArray, 0, networkInterface);
314
- java.lang.reflect.Array.set(ifaceArray, 1, emulatorInterface);
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());
315
545
  scanMethod.invoke(discoveryFactory, discoveryListener, ifaceArray);
316
- Log.i(TAG, "[FLIR SDK] (DEX) scan invoked on DiscoveryFactory");
317
- Log.i(TAG, "[FLIR SDK] (DEX) Started discovery scan for NETWORK and EMULATOR");
318
- return true;
546
+
547
+ logStep("EMULATOR_SCAN_STARTED", "Scanning for emulator devices...");
548
+ Log.i(TAG, "[FLIR] Emulator discovery started");
549
+
319
550
  } catch (Throwable t) {
320
- Log.w(TAG, "[FLIR SDK] startSdkEmulatorWithClassLoader failed: " + t.getMessage());
321
- return false;
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());
322
554
  }
323
555
  }
324
-
325
- private void connectToSdkCamera(Object discoveredCamera) {
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) {
326
581
  try {
582
+ // Get identity from DiscoveredCamera
327
583
  Method getIdentity = discoveredCamera.getClass().getMethod("getIdentity");
328
584
  Object identity = getIdentity.invoke(discoveredCamera);
329
-
330
- Class<?> cameraClass = findSdkClass("com.flir.thermalsdk.live.Camera");
331
- cameraObj = cameraClass.newInstance();
332
-
333
- Class<?> connStatusClass = findSdkClass("com.flir.thermalsdk.live.connectivity.ConnectionStatusListener");
334
- Object connListener = Proxy.newProxyInstance(connStatusClass.getClassLoader(),
335
- new Class<?>[] { connStatusClass }, (proxy, method, args) -> {
336
- if (method.getName().equals("onDisconnected")) {
337
- Log.w(TAG, "[FLIR SDK] Camera disconnected");
338
- cameraObj = null;
339
- streamerObj = null;
340
- }
341
- return null;
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);
342
616
  });
343
-
344
- // Try various connect overloads to be compatible across SDK versions
345
- boolean connected = false;
346
- try {
347
- Class<?> connectParamsClass = findSdkClass("com.flir.thermalsdk.live.ConnectParameters");
348
- Object connectParams = null;
349
- try {
350
- connectParams = connectParamsClass.newInstance();
351
- } catch (Throwable ignored) {
352
617
  }
353
- Method connectMethod3 = cameraClass.getMethod("connect", identity.getClass(), connStatusClass,
354
- connectParamsClass);
355
- connectMethod3.invoke(cameraObj, identity, connListener, connectParams);
356
- connected = true;
357
- } catch (Throwable ignored) {
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)
358
728
  try {
359
- Method connectMethod2 = cameraClass.getMethod("connect", identity.getClass(), connStatusClass);
360
- connectMethod2.invoke(cameraObj, identity, connListener);
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);
361
735
  connected = true;
362
- } catch (Throwable ignored2) {
736
+ } catch (NoSuchMethodException ignored) {}
737
+
738
+ // Try connect(Identity, ConnectionStatusListener)
739
+ if (!connected) {
363
740
  try {
364
- Method connectSimple = cameraClass.getMethod("connect", identity.getClass());
365
- connectSimple.invoke(cameraObj, identity);
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);
366
745
  connected = true;
367
- } catch (Throwable ignored3) {
368
- Log.w(TAG, "connect: All connect attempts failed: " + ignored3.getMessage());
369
- }
746
+ } catch (NoSuchMethodException ignored) {}
370
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());
371
783
  }
372
-
373
- String deviceName = "EMULATOR";
374
- try {
375
- Method getName = identity.getClass().getMethod("getName");
376
- Object nm = getName.invoke(identity);
377
- if (nm != null)
378
- deviceName = nm.toString();
379
- } catch (Throwable ignored) {
380
- }
381
-
382
- if (listener != null) {
383
- listener.onDeviceFound(deviceName);
384
- listener.onEmulatorEnabled();
385
- }
386
- Log.d(TAG, "[FLIR SDK] Scheduling startSdkStreaming; cameraObj=" + cameraObj + ", identity=" + identity
387
- + ", connected=" + connected);
388
- scheduler.schedule(this::startSdkStreaming, 500, TimeUnit.MILLISECONDS);
389
- } catch (Throwable t) {
390
- Log.w(TAG, "connectToSdkCamera failed: " + t.getMessage(), t);
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());
391
797
  }
392
798
  }
393
-
394
- private void startSdkStreaming() {
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
+
395
812
  try {
396
- Log.d(TAG, "[FLIR SDK] startSdkStreaming invoked; cameraObj=" + cameraObj + ", streamerObj=" + streamerObj);
397
- if (cameraObj == null)
398
- return;
813
+ // Get camera streams
399
814
  Method getStreams = cameraObj.getClass().getMethod("getStreams");
400
815
  Object streams = getStreams.invoke(cameraObj);
401
- if (streams == null)
402
- Log.i(TAG, "[FLIR SDK] No streams returned from camera.getStreams()");
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
403
828
  Object chosenStream = null;
404
- int streamCount = 0;
405
- if (streams instanceof List) {
406
- List<?> streamList = (List<?>) streams;
407
- streamCount = streamList.size();
408
- Log.i(TAG, "[FLIR SDK] getStreams returned list with size=" + streamList.size());
409
- for (Object s : streamList) {
410
- if (s == null)
411
- continue;
412
- try {
413
- Method getName = null;
414
- try {
415
- getName = s.getClass().getMethod("getName");
416
- } catch (Throwable ignored) {
417
- }
418
- String name = null;
419
- if (getName != null)
420
- name = getName.invoke(s).toString().toLowerCase();
421
-
422
- Method getType = null;
423
- try {
424
- getType = s.getClass().getMethod("getStreamType");
425
- } catch (Throwable ignored) {
426
- }
427
- if (getType == null)
428
- try {
429
- getType = s.getClass().getMethod("getType");
430
- } catch (Throwable ignored) {
431
- }
432
- String type = null;
433
- if (getType != null)
434
- type = getType.invoke(s).toString().toLowerCase();
435
-
436
- String test = (name != null ? name : "") + "|" + (type != null ? type : "");
437
- Log.d(TAG, "[FLIR SDK] stream candidate: name='" + name + "', type='" + type + "', test='"
438
- + test + "'");
439
- if (test.contains("fusion") || test.contains("palette") || test.contains("visual")
440
- || test.contains("msx")) {
441
- chosenStream = s;
442
- break;
443
- }
444
- } catch (Throwable ignored) {
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";
445
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");
446
879
  }
447
- if (chosenStream == null && !streamList.isEmpty())
448
- chosenStream = streamList.get(0);
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);
449
885
  }
450
- if (chosenStream == null)
451
- Log.w(TAG, "[FLIR SDK] No chosen stream found; streamCount=" + streamCount);
452
- if (chosenStream == null)
453
- return;
454
-
455
- Class<?> thermalStreamerClass = findSdkClass("com.flir.thermalsdk.live.streaming.ThermalStreamer");
456
- java.lang.reflect.Constructor<?> ctor = thermalStreamerClass
457
- .getConstructor(findSdkClass("com.flir.thermalsdk.live.streaming.Stream"));
458
- streamerObj = ctor.newInstance(chosenStream);
459
- Log.i(TAG, "[FLIR SDK] ThermalStreamer instance created: "
460
- + (streamerObj != null ? streamerObj.getClass().getName() : "null"));
461
-
462
- boolean listenerAttached = attachStreamerListener(streamerObj);
463
- if (!listenerAttached)
464
- startFramePolling();
465
- Log.i(TAG, "[FLIR SDK] startSdkStreaming: listenerAttached=" + listenerAttached + ", streamerObj="
466
- + (streamerObj != null));
467
-
468
- // Notify stream kind change
469
- try {
470
- Method getName = chosenStream.getClass().getMethod("getName");
471
- Object nm = getName.invoke(chosenStream);
472
- if (nm != null && listener != null)
473
- listener.onStreamKindChanged(nm.toString());
474
- try {
475
- sdkCurrentStreamKind = nm != null ? nm.toString() : null;
476
- } catch (Throwable ignored) {
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;
477
897
  }
478
- } catch (Throwable ignored) {
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));
479
926
  }
480
-
927
+
481
928
  } catch (Throwable t) {
482
- Log.w(TAG, "startSdkStreaming failed: " + t.getMessage(), 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());
483
932
  }
484
933
  }
485
-
486
- public boolean forceStartStreaming() {
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
+
487
949
  try {
488
- if (cameraObj == null) {
489
- Log.i(TAG, "[FLIR SDK] forceStartStreaming: No cameraObj, attempting discovery scan");
490
- try {
491
- // Re-run discovery to find cameras
492
- Class<?> commIfaceClass = findSdkClass("com.flir.thermalsdk.live.CommunicationInterface");
493
- Object emulatorInterface = Enum.valueOf((Class<Enum>) commIfaceClass, "EMULATOR");
494
- Class<?> discoveryFactoryClass = findSdkClass(
495
- "com.flir.thermalsdk.live.discovery.DiscoveryFactory");
496
- Method getInstance = discoveryFactoryClass.getMethod("getInstance");
497
- Object df = getInstance.invoke(null);
498
- Class<?> listenerClass = findSdkClass("com.flir.thermalsdk.live.discovery.DiscoveryEventListener");
499
- Object discoveryListener = Proxy.newProxyInstance(
500
- sdkClassLoader != null ? sdkClassLoader : discoveryFactoryClass.getClassLoader(),
501
- new Class<?>[] { listenerClass },
502
- (proxy, method, args) -> {
503
- String methodName = method.getName();
504
- if ("onCameraFound".equalsIgnoreCase(methodName) && args != null && args.length > 0) {
505
- try {
506
- Object discovered = args[0];
507
- Log.i(TAG, "[FLIR SDK] forceStartStreaming: discovered camera: " + discovered);
508
- connectToSdkCamera(discovered);
509
- } catch (Throwable t) {
510
- Log.w(TAG, "[FLIR SDK] forceStartStreaming onCameraFound handler error: "
511
- + t.getMessage());
512
- }
513
- }
514
- return null;
515
- });
516
- Method scanMethod = discoveryFactoryClass.getMethod("scan", listenerClass,
517
- java.lang.reflect.Array.newInstance(commIfaceClass, 0).getClass());
518
- Object ifaceArray = java.lang.reflect.Array.newInstance(commIfaceClass, 1);
519
- java.lang.reflect.Array.set(ifaceArray, 0, emulatorInterface);
520
- scanMethod.invoke(df, discoveryListener, ifaceArray);
521
- } catch (Throwable t) {
522
- Log.w(TAG, "[FLIR SDK] forceStartStreaming discovery scan failed: " + t.getMessage());
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);
523
986
  }
524
- // if cameraObj still null - return false
525
- if (cameraObj == null)
526
- return false;
527
987
  }
528
- // If cameraObj present, start streaming
529
- scheduler.submit(() -> startSdkStreaming());
530
- Log.i(TAG, "[FLIR SDK] forceStartStreaming scheduled startSdkStreaming");
531
- return true;
988
+
532
989
  } catch (Throwable t) {
533
- Log.w(TAG, "[FLIR SDK] forceStartStreaming failed: " + t.getMessage(), t);
534
- return false;
990
+ // Log errors periodically to avoid spam
991
+ if (System.currentTimeMillis() % 5000 < 100) {
992
+ Log.d(TAG, "[FLIR] Frame processing error: " + t.getMessage());
993
+ }
535
994
  }
536
995
  }
537
-
538
- private void startFramePolling() {
539
- scheduler.scheduleAtFixedRate(() -> {
540
- try {
541
- if (streamerObj == null)
542
- return;
543
- Log.d(TAG, "[FLIR SDK] startFramePolling - streamerObj class=" + streamerObj.getClass().getName());
544
- Method updateMethod = streamerObj.getClass().getMethod("update");
545
- updateMethod.invoke(streamerObj);
546
-
547
- Method getImage = streamerObj.getClass().getMethod("getImage");
548
- Object thermalImage = getImage.invoke(streamerObj);
549
- if (thermalImage != null) {
550
- try {
551
- // Prefer: direct getBitmap() if available
552
- try {
553
- Method getBitmap = thermalImage.getClass().getMethod("getBitmap");
554
- Object bmp = getBitmap.invoke(thermalImage);
555
- if (bmp instanceof Bitmap) {
556
- latestFrame = (Bitmap) bmp;
557
- if (listener != null) {
558
- listener.onFrame((Bitmap) bmp);
559
- Log.d(TAG, "[FLIR SDK] Frame emitted from polling: " + ((Bitmap) bmp).getWidth()
560
- + "x" + ((Bitmap) bmp).getHeight());
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");
561
1061
  }
562
- return;
563
1062
  }
564
- } catch (Throwable ignored) {
565
- }
566
-
567
- // Fallback: try using
568
- // BitmapAndroid.createBitmap(thermalImage.getImage()).getBitMap()
569
- try {
570
- Object imageBase = null;
1063
+
1064
+ // Step 2: Convert to bitmap INSIDE the callback
1065
+ // This is critical - must happen while we have access to thermal image context
571
1066
  try {
572
- Method getImageFromThermal = thermalImage.getClass().getMethod("getImage");
573
- imageBase = getImageFromThermal.invoke(thermalImage);
574
- } catch (Throwable ignored) {
575
- }
576
- if (imageBase == null)
577
- imageBase = thermalImage;
578
- Class<?> bitmapAndroidClass = findSdkClass(
579
- "com.flir.thermalsdk.androidsdk.image.BitmapAndroid");
580
- Method createBitmap = bitmapAndroidClass.getMethod("createBitmap", imageBase.getClass());
581
- Object wrapper = createBitmap.invoke(null, imageBase);
582
- if (wrapper != null) {
583
- Method getBitMapMethod = wrapper.getClass().getMethod("getBitMap");
584
- Object bmp = getBitMapMethod.invoke(wrapper);
585
- if (bmp instanceof Bitmap) {
586
- latestFrame = (Bitmap) bmp;
587
- if (listener != null)
588
- listener.onFrame((Bitmap) bmp);
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());
589
1083
  }
1084
+ Log.d(TAG, "[FLIR] Bitmap conversion in callback failed: " + t.getMessage());
590
1085
  }
591
- } catch (Throwable ignored) {
1086
+ } else if (frameCount <= 5) {
1087
+ logStep("THERMAL_IMAGE_NULL", "ThermalImage is null in callback");
592
1088
  }
593
- } catch (Throwable ignored) {
594
1089
  }
1090
+ return null;
595
1091
  }
596
- } catch (Throwable t) {
597
- if (System.currentTimeMillis() % 5000 < 200)
598
- Log.d(TAG, "Frame poll error: " + t.getMessage());
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);
599
1103
  }
600
- }, 100, 100, TimeUnit.MILLISECONDS);
601
- }
602
-
603
- Bitmap getLatestFrame() {
604
- return latestFrame;
1104
+ }
605
1105
  }
606
-
607
- double getTemperatureAtPoint(int x, int y) {
608
- // Try to query using SDK streamer if available
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
+
609
1115
  try {
610
- if (streamerObj != null) {
611
- Method getImage = streamerObj.getClass().getMethod("getImage");
612
- Object thermalImage = getImage.invoke(streamerObj);
613
- if (thermalImage != null) {
614
- try {
615
- Method getValueAt = thermalImage.getClass().getMethod("getValueAt",
616
- android.graphics.Point.class);
617
- android.graphics.Point p = new android.graphics.Point(x, y);
618
- Object temp = getValueAt.invoke(thermalImage, p);
619
- if (temp instanceof Double)
620
- return (Double) temp;
621
- if (temp instanceof Float)
622
- return ((Float) temp).doubleValue();
623
- } catch (NoSuchMethodException nsme) {
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);
624
1136
  }
625
- try {
626
- Method getValues = thermalImage.getClass().getMethod("getValues");
627
- Object values = getValues.invoke(thermalImage);
628
- if (values != null) {
629
- Method valGetAt = values.getClass().getMethod("getValueAt", int.class, int.class);
630
- Object temp = valGetAt.invoke(values, x, y);
631
- if (temp instanceof Double)
632
- return (Double) temp;
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);
633
1151
  }
634
- } catch (Throwable ignored) {
635
1152
  }
1153
+ return null;
636
1154
  }
637
- }
1155
+ );
1156
+
1157
+ withThermalImageMethod.invoke(streamerObj, consumer);
1158
+
638
1159
  } catch (Throwable t) {
1160
+ Log.d(TAG, "[FLIR] applyPaletteViaThermalImage failed: " + t.getMessage());
639
1161
  }
640
- return Double.NaN;
641
1162
  }
642
-
643
- boolean setPalette(String paletteName) {
1163
+
1164
+ /**
1165
+ * Set palette on a ThermalImage object
1166
+ */
1167
+ private void setPaletteOnThermalImage(Object thermalImage) {
1168
+ if (thermalImage == null || currentPalette == null) return;
1169
+
644
1170
  try {
645
- if (streamerObj == null && cameraObj == null)
646
- return false;
647
- Object thermalImage = null;
648
- if (streamerObj != null) {
649
- Method getImage = streamerObj.getClass().getMethod("getImage");
650
- thermalImage = getImage.invoke(streamerObj);
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());
651
1192
  }
652
- if (thermalImage != null) {
653
- Class<?> paletteManagerClass = null;
654
- try {
655
- paletteManagerClass = findSdkClass("com.flir.thermalsdk.image.palettes.PaletteManager");
656
- } catch (Throwable ignored) {
657
- try {
658
- paletteManagerClass = findSdkClass("com.flir.thermalsdk.image.PaletteManager");
659
- } catch (Throwable ignored2) {
660
- }
661
- }
662
- if (paletteManagerClass == null)
663
- return false;
664
- Method getDefaultPalettes = paletteManagerClass.getMethod("getDefaultPalettes");
665
- Object palettesResult = getDefaultPalettes.invoke(null);
666
- Object target = null;
667
- // Handle array or List return types
668
- if (palettesResult instanceof Object[]) {
669
- for (Object p : (Object[]) palettesResult) {
670
- try {
671
- Method getName = p.getClass().getMethod("getName");
672
- String n = (String) getName.invoke(p);
673
- if (n != null && n.equalsIgnoreCase(paletteName)) {
674
- target = p;
675
- break;
676
- }
677
- } catch (Throwable ignored) {
678
- }
679
- }
680
- } else if (palettesResult instanceof java.util.List) {
681
- for (Object p : (java.util.List) palettesResult) {
682
- try {
683
- Method getName = p.getClass().getMethod("getName");
684
- String n = (String) getName.invoke(p);
685
- if (n != null && n.equalsIgnoreCase(paletteName)) {
686
- target = p;
687
- break;
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());
688
1203
  }
689
- } catch (Throwable ignored) {
690
- }
691
- }
692
- } else if (palettesResult != null && palettesResult.getClass().isArray()) {
693
- int len = java.lang.reflect.Array.getLength(palettesResult);
694
- for (int i = 0; i < len; i++) {
695
- Object p = java.lang.reflect.Array.get(palettesResult, i);
696
- try {
697
- Method getName = p.getClass().getMethod("getName");
698
- String n = (String) getName.invoke(p);
699
- if (n != null && n.equalsIgnoreCase(paletteName)) {
700
- target = p;
701
- break;
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) {}
702
1223
  }
703
- } catch (Throwable ignored) {
704
- }
705
- }
706
- }
707
- if (target != null) {
708
- Class<?> paletteClass = null;
709
- try {
710
- paletteClass = findSdkClass("com.flir.thermalsdk.image.palettes.Palette");
711
- } catch (Throwable ignored) {
712
- try {
713
- paletteClass = findSdkClass("com.flir.thermalsdk.image.Palette");
714
- } catch (Throwable ignored2) {
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) {}
715
1233
  }
716
1234
  }
717
- if (paletteClass == null)
718
- return false;
719
- Method setPalette = thermalImage.getClass().getMethod("setPalette", paletteClass);
720
- setPalette.invoke(thermalImage, target);
721
- return true;
722
1235
  }
723
1236
  }
1237
+
1238
+ Log.d(TAG, "[FLIR] BitmapAndroid.createBitmap method not found for imageBuffer type: " + imageBuffer.getClass().getName());
1239
+
724
1240
  } catch (Throwable t) {
725
- Log.w(TAG, "setPalette failed: " + t.getMessage());
1241
+ Log.d(TAG, "[FLIR] convertToBitmap primary method failed: " + t.getMessage());
726
1242
  }
727
- return false;
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;
728
1254
  }
729
-
730
- public boolean isStreamingActive() {
731
- return streamerObj != null;
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);
732
1270
  }
733
-
734
- public String getSdkCurrentStreamKind() {
735
- return sdkCurrentStreamKind;
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;
736
1294
  }
737
-
738
- private boolean attachStreamerListener(Object streamer) {
739
- if (streamer == null)
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");
740
1311
  return false;
1312
+ }
1313
+
741
1314
  try {
742
- Class<?> streamerClass = streamer.getClass();
743
- String[] candidates = new String[] { "setFrameListener", "setListener", "addListener", "addFrameListener",
744
- "addStreamListener", "setOnImageAvailableListener", "addImageListener", "setDataListener",
745
- "subscribe" };
746
- for (String name : candidates) {
747
- try {
748
- for (Method m : streamerClass.getMethods()) {
749
- if (!m.getName().equalsIgnoreCase(name))
750
- continue;
751
- Class<?>[] params = m.getParameterTypes();
752
- if (params.length != 1 || !params[0].isInterface())
753
- continue;
754
- final Class<?> listenerInterface = params[0];
755
- Object proxy = Proxy.newProxyInstance(listenerInterface.getClassLoader(),
756
- new Class<?>[] { listenerInterface }, (proxyObj, method, args) -> {
757
- try {
758
- Log.d(TAG, "[FLIR SDK] Stream listener method invoked: " + method.getName());
759
- if (args != null && args.length > 0) {
760
- for (Object arg : args) {
761
- if (arg == null)
762
- continue;
763
- try {
764
- Method getFusion = arg.getClass().getMethod("getFusion");
765
- if (getFusion != null) {
766
- Object fusion = getFusion.invoke(arg);
767
- if (fusion != null) {
768
- try {
769
- Method getPhoto = fusion.getClass()
770
- .getMethod("getPhoto");
771
- Object photo = getPhoto.invoke(fusion);
772
- if (photo != null) {
773
- try {
774
- Method pb = photo.getClass()
775
- .getMethod("getBitmap");
776
- Object bmpObj = pb.invoke(photo);
777
- if (bmpObj instanceof Bitmap) {
778
- if (listener != null) {
779
- listener.onFrame((Bitmap) bmpObj);
780
- Log.d(TAG,
781
- "[FLIR SDK] Streamer listener delivered bitmap via getBitmap");
782
- }
783
- return null;
784
- }
785
- } catch (Throwable ignored) {
786
- }
787
- }
788
- } catch (Throwable ignored) {
789
- }
790
- }
791
- }
792
- } catch (Throwable ignored) {
793
- }
794
- try {
795
- Method getBitmap = arg.getClass().getMethod("getBitmap");
796
- Object bmp = getBitmap.invoke(arg);
797
- if (bmp instanceof Bitmap) {
798
- if (listener != null) {
799
- listener.onFrame((Bitmap) bmp);
800
- Log.d(TAG,
801
- "[FLIR SDK] Streamer listener delivered bitmap via direct getBitmap");
802
- }
803
- return null;
804
- }
805
- } catch (Throwable ignored) {
806
- }
807
- try {
808
- Method getImage = arg.getClass().getMethod("getImage");
809
- Object imgObj = getImage.invoke(arg);
810
- if (imgObj instanceof Bitmap) {
811
- if (listener != null) {
812
- listener.onFrame((Bitmap) imgObj);
813
- Log.d(TAG,
814
- "[FLIR SDK] Streamer listener delivered bitmap via getImage");
815
- }
816
- return null;
817
- }
818
- // fallback to BitmapAndroid.createBitmap(imgObj).getBitMap()
819
- try {
820
- Class<?> bitmapAndroidClass = findSdkClass(
821
- "com.flir.thermalsdk.androidsdk.image.BitmapAndroid");
822
- Method createBitmap = bitmapAndroidClass
823
- .getMethod("createBitmap", imgObj.getClass());
824
- Object wrapper = createBitmap.invoke(null, imgObj);
825
- if (wrapper != null) {
826
- Method getBitMap = wrapper.getClass()
827
- .getMethod("getBitMap");
828
- Object bmpObj = getBitMap.invoke(wrapper);
829
- if (bmpObj instanceof Bitmap) {
830
- if (listener != null) {
831
- listener.onFrame((Bitmap) bmpObj);
832
- Log.d(TAG,
833
- "[FLIR SDK] Streamer listener delivered bitmap via BitmapAndroid wrapper");
834
- }
835
- return null;
836
- }
837
- }
838
- } catch (Throwable ignored2) {
839
- }
840
- } catch (Throwable ignored) {
841
- }
842
- }
843
- }
844
- } catch (Throwable t) {
845
- Log.w(TAG, "Streamer listener failed: " + t.getMessage());
846
- }
847
- return null;
848
- });
849
- try {
850
- m.invoke(streamer, proxy);
851
- } catch (Throwable e) {
852
- Log.w(TAG, "attach proxy failed: " + e.getMessage(), e);
853
- continue;
854
- }
855
- Log.i(TAG, "Attached streamer listener using: " + m.getName());
856
- return true;
857
- }
858
- } catch (Throwable ignored) {
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;
859
1377
  }
860
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
+
861
1437
  } catch (Throwable t) {
862
- Log.w(TAG, "attachStreamerListener error: " + t.getMessage());
1438
+ Log.e(TAG, "[FLIR SDK] attemptLoadSdkFromAar failed: " + t.getMessage(), t);
863
1439
  }
1440
+
864
1441
  return false;
865
1442
  }
866
-
867
- public void stop() {
1443
+
1444
+ private Class<?> findSdkClass(String name) throws ClassNotFoundException {
868
1445
  try {
869
- scheduler.shutdownNow();
870
- if (streamerObj != null) {
871
- try {
872
- Method stop = streamerObj.getClass().getMethod("stop");
873
- if (stop != null)
874
- stop.invoke(streamerObj);
875
- } catch (Throwable ignored) {
876
- }
1446
+ return Class.forName(name);
1447
+ } catch (ClassNotFoundException e) {
1448
+ if (sdkClassLoader != null) {
1449
+ return Class.forName(name, true, sdkClassLoader);
877
1450
  }
878
- if (cameraObj != null) {
879
- try {
880
- Method disconnect = cameraObj.getClass().getMethod("disconnect");
881
- if (disconnect != null)
882
- disconnect.invoke(cameraObj);
883
- } catch (Throwable ignored) {
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();
884
1479
  }
885
- }
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";
886
1486
  } catch (Throwable t) {
887
- Log.w(TAG, "Failed to stop SDK manager: " + t.getMessage());
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));
888
1509
  }
889
1510
  }
890
1511
  }