// Copyright (C) 2025 Acoustic, L.P. All rights reserved. // // NOTICE: This file contains material that is confidential and proprietary to // Acoustic, L.P. and/or other developers. No license is granted under any intellectual or // industrial property rights of Acoustic, L.P. except as may be provided in an agreement with // Acoustic, L.P. Any unauthorized copying or distribution of content from this file is // prohibited. // // // Created by Omar Hernandez on 5/9/25. // package com.acousticconnectrn import android.app.Activity import android.app.Application import android.content.Context import android.os.Handler import android.os.Looper import android.text.TextUtils import android.view.View import android.view.View.OnFocusChangeListener import android.view.inputmethod.InputMethodManager import android.widget.EditText import android.widget.TextView import com.acoustic.connect.android.connectmod.Connect import com.acoustic.connect.android.connectmod.Connect.TLF_ON_FOCUS_CHANGE_IN import com.acoustic.connect.android.connectmod.Connect.TLF_ON_FOCUS_CHANGE_OUT import com.acoustic.connect.android.connectmod.Connect.TLF_UI_KEYBOARD_DID_HIDE_NOTIFICATION import com.acoustic.connect.android.connectmod.Connect.TLF_UI_KEYBOARD_DID_SHOW_NOTIFICATION import com.acoustic.connect.android.connectmod.Connect.enable import com.acoustic.connect.android.connectmod.Connect.getApplication import com.acoustic.connect.android.connectmod.Connect.init import com.acoustic.connect.android.connectmod.Connect.isEnabled import com.acoustic.connect.android.connectmod.Connect.logEvent import com.acoustic.connect.android.connectmod.Connect.logGeolocation import com.acoustic.connect.android.connectmod.Connect.logLocationUpdateEventWithLatitude import com.acoustic.connect.android.connectmod.Connect.logScreenLayout import com.acoustic.connect.android.connectmod.Connect.logScreenview import com.acoustic.connect.android.connectmod.Connect.onResume import com.acoustic.connect.android.connectmod.Connect.registerFormField import com.acoustic.connect.android.connectmod.Connect.resumeConnect import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.uimanager.NativeViewHierarchyManager import com.facebook.react.uimanager.UIManagerModule import com.ibm.eo.EOCore import com.ibm.eo.model.EOMonitoringLevel import com.margelo.nitro.NitroModules import com.margelo.nitro.acousticconnectrn.HybridAcousticConnectRNSpec import com.margelo.nitro.acousticconnectrn.Variant_String_Double_Boolean import com.tl.uic.Tealeaf import com.tl.uic.model.ScreenviewType import com.tl.uic.util.LayoutUtil import com.tl.uic.util.keyboardview.KeyboardView import java.util.Objects class HybridAcousticConnectRN(private val reactContext: ReactApplicationContext) : HybridAcousticConnectRNSpec(), LifecycleEventListener { init { // Add your listener logic here println("HybridAcousticConnectRNSpec has been initialized") Handler(Looper.getMainLooper()).post { if (reactContext.hasActiveCatalystInstance()) { reactContext.addLifecycleEventListener(this) } else { println("ReactContext is not ready. LifecycleEventListener not registered.") } } } /** * Sets the module's boolean configuration item from AdvancedConfig.json or BasicConfig.properties that matches the specified key. * * @param key Map Key. * @param value Boolean Value. * @param moduleName The class name of the module's EOLifecycleObject for which the configuration item is referencing. * @return True if the operation was successful, false otherwise. */ override fun setBooleanConfigItemForKey( key: String, value: Boolean, moduleName: String ): Boolean { val result: Boolean = EOCore.updateConfig(key, value.toString(), EOCore.getLifecycleObject(moduleName)) return result } /** * Sets the module's string configuration item from AdvancedConfig.json or BasicConfig.properties that matches the specified key. * * @param key Map Key. * @param value String Value. * @param moduleName The class name of the module's EOLifecycleObject for which the configuration item is referencing. * @return True if the operation was successful, false otherwise. */ override fun setStringItemForKey( key: String, value: String, moduleName: String ): Boolean { val result = EOCore.updateConfig(key, value, EOCore.getLifecycleObject(moduleName)) return result } /** * Sets the module's number configuration item from AdvancedConfig.json or BasicConfig.properties that matches the specified key. * * @param key Map Key. * @param value Number Value. * @param moduleName The class name of the module's EOLifecycleObject for which the configuration item is referencing. * @return True if the operation was successful, false otherwise. */ override fun setNumberItemForKey( key: String, value: Double, moduleName: String ): Boolean { val result = EOCore.updateConfig(key, value.toString(), EOCore.getLifecycleObject(moduleName)) return result } /** * Sets the module's configuration item from AdvancedConfig.json or BasicConfig.properties that matches the specified key. * * @param key Map Key. * @param value Map Value. * @param moduleName The class name of the module's EOLifecycleObject for which the configuration item is referencing. * @param promise Javascript Promise interface. */ override fun setConfigItemForKey( key: String, value: Variant_String_Double_Boolean, moduleName: String ): Boolean { val result = EOCore.updateConfig(key, value.toString(), EOCore.getLifecycleObject(moduleName)) return result } /** * Gets the module's configuration item from AdvancedConfig.json or BasicConfig.properties that matches the specified key as a BOOL value. * * @param theDefault In case no value if found, use this value as default. * @param key Key value. * @param moduleName The class name of the module's EOLifecycleObject for which the configuration item is referencing. * @return True if the operation was successful, false otherwise. */ override fun getBooleanConfigItemForKey( theDefault: Boolean, key: String, moduleName: String ): Boolean { val result = EOCore.getConfigItemBoolean(key, EOCore.getLifecycleObject(moduleName)) if (result == false) { return theDefault } return result } /** * Gets the module's configuration item from AdvancedConfig.json or BasicConfig.properties that matches the specified key as a String value. * * @param theDefault In case no value if found, use this value as default. * @param key Key value. * @param moduleName The class name of the module's EOLifecycleObject for which the configuration item is referencing. * @return String value if the operation was successful, null otherwise. */ override fun getStringItemForKey(theDefault: String, key: String, moduleName: String): String? { var result = EOCore.getConfigItemString(key, EOCore.getLifecycleObject(moduleName)) if (TextUtils.isEmpty(result)) { result = theDefault } return result } /** * Gets the module's configuration item from AdvancedConfig.json or BasicConfig.properties that matches the specified key as a double value. * * @param theDefault In case no value if found, use this value as default. * @param key Key value. * @param moduleName The class name of the module's EOLifecycleObject for which the configuration item is referencing. * @return Double value if the operation was successful, 0.0 otherwise. */ override fun getNumberItemForKey(theDefault: Double, key: String, moduleName: String): Double { var result = EOCore.getConfigItemDouble(key, EOCore.getLifecycleObject(moduleName)) if (result == -1.0) { result = theDefault } return result } /** * Logs a custom event with the specified name and values. * * @param eventName The name of the event to be logged this will appear in the posted json. * @param values A map of values associated with the event. * @param level Set a custom log level to the event. This will override the configured log level for that event. * @return True if the operation was successful, false otherwise. */ override fun logCustomEvent( eventName: String, values: Map, level: Double ): Boolean { val result = Connect.logCustomEvent(eventName, convertToMap(values), level.toInt()) return result } /** * Logs a signal with the specified values. * * @param values A map of values associated with the signal. * @param level Set a custom log level to the event. This will override the configured log level for that event. * @return True if the operation was successful, false otherwise. */ override fun logSignal( values: Map, level: Double ): Boolean { val result = Connect.logSignal(convertToMapAny(values), level.toInt()) return result } /** * Logs an exception event with the specified message and stack information. * * @param message The message associated with the exception. * @param stackInfo The stack information associated with the exception. * @param unhandled Indicates whether the exception is unhandled. * @return True if the operation was successful, false otherwise. */ override fun logExceptionEvent(message: String, stackInfo: String, unhandled: Boolean): Boolean { val result = Connect.logExceptionEvent("React Plugin", message, stackInfo, unhandled) return result } /** * Logs the current location. * * @return True if the operation was successful, false otherwise. */ override fun logLocation(): Boolean { val result = logGeolocation(EOMonitoringLevel.kEOMonitoringLevelInfo.value) return result } /** * Logs the current location with the specified latitude and longitude. * * @param latitude The latitude of the location. * @param longitude The longitude of the location. * @param level Set a custom log level to the event. This will override the configured log level for that event. * @return True if the operation was successful, false otherwise. */ override fun logLocationWithLatitudeLongitude( latitude: Double, longitude: Double, level: Double ): Boolean { val result = logLocationUpdateEventWithLatitude(latitude, longitude, level.toInt()) return result } /** * Log click events on react native control. * * @param target Target id of the control. * @param controlId Accessibility ID(virtual id). * @return True if the operation was successful, false otherwise. */ override fun logClickEvent(target: Double, controlId: String): Boolean { var result = false try { val context: ReactApplicationContext = reactContext // Add UI-block so we can get a valid reference to the map-view val uiManager = context.getNativeModule(UIManagerModule::class.java) Objects.requireNonNull(uiManager) .addUIBlock { nvhm: NativeViewHierarchyManager -> val view = nvhm.resolveView(target.toInt()) if (view == null) { result = false } else { if (view is EditText) { addFocusAndRegister(view, null, getCurrentActivity()!!) } else { if (!TextUtils.isEmpty(controlId)) { logEvent(view, "click", controlId) } else { logEvent(view, "click") } } result = true } } } catch (e: java.lang.Exception) { // Handle the exception result = false } return result } /** * Log click events on react native control. * * @param target Target id of the control. * @return True if the operation was successful, false otherwise. */ fun logClickEvent(target: Double): Boolean { var result = false try { val context: ReactApplicationContext = reactContext // Add UI-block so we can get a valid reference to the map-view val uiManager = context.getNativeModule(UIManagerModule::class.java) Objects.requireNonNull(uiManager) .addUIBlock { nvhm: NativeViewHierarchyManager -> val view = nvhm.resolveView(target.toInt()) if (view == null) { result = false } else { if (view is EditText) { addFocusAndRegister(view as EditText, null, getCurrentActivity()!!) } else { logEvent(view, "click") } result = true } } } catch (e: java.lang.Exception) { // Handle the exception result = false } return result } /** * Log EditText change event. * * @param target A valid native View Id for lookup. * @param controlId Accessibility ID(virtual id). * @param text The input string. * @return True if the operation was successful, false otherwise. */ override fun logTextChangeEvent(target: Double, controlId: String, text: String?): Boolean { var result = false try { val context: ReactApplicationContext = reactContext // Add UI-block so we can get a valid reference to the map-view val uiManager = context.getNativeModule(UIManagerModule::class.java) Objects.requireNonNull(uiManager) .addUIBlock { nvhm: NativeViewHierarchyManager -> val view = nvhm.resolveView(target.toInt()) if (view == null) { result = false } else { if (view is EditText && (view as EditText).onFocusChangeListener == null) { // First time, logEvent and subsequent calls will be handled in change listener logEvent(view, TLF_ON_FOCUS_CHANGE_IN, controlId) addFocusAndRegister(view as EditText, controlId, getCurrentActivity()!!) } result = true } } } catch (e: java.lang.Exception) { // Handle the exception result = false } return result } /** * Requests that the framework save the current application page name. * * @param logicalPageName The logical page name to be set. * @return True if the operation was successful, false otherwise. */ override fun setCurrentScreenName(logicalPageName: String): Boolean { val result = resumeConnect(getCurrentActivity(), logicalPageName, false) return result } /** * Requests that the framework logs an screen load event. * * @param logicalPageName The logical page name to be set. * @param referrer The referrer for the screen view. * @return True if the operation was successful, false otherwise. */ override fun logScreenViewContextLoad(logicalPageName: String?, referrer: String?): Boolean { val result = logScreenview(getCurrentActivity()!!, logicalPageName.toString(), ScreenviewType.LOAD, referrer); return result } /** * Requests that the framework logs an screen unload event. * * @param logicalPageName The logical page name to be set. * @param referrer The referrer for the screen view. * @return True if the operation was successful, false otherwise. */ override fun logScreenViewContextUnload(logicalPageName: String?, referrer: String?): Boolean { val result = logScreenview(getCurrentActivity()!!, logicalPageName.toString(), ScreenviewType.UNLOAD, referrer); return result } /** * Log Current Screen Layout using native side background thread. * * @param name Page name or title e.g. "Login View Controller"; Must not be empty. * @param delay The delay in milliseconds before logging the event. * @return True if the operation was successful, false otherwise. */ override fun logScreenLayout(name: String, delay: Double): Boolean { setCurrentScreenName(name) logScreenview(getCurrentActivity(), name, ScreenviewType.LOAD) var result = false if (LayoutUtil.canCaptureUserEvents(null, name)) { result = logScreenLayout( Objects.requireNonNull(getCurrentActivity()), name, if (delay.toInt() < 0) 300 else delay.toInt(), true ) } return result } /** * Converts a map of Variant_String_Double_Boolean to a HashMap. * * @param values The map to be converted. * @return A HashMap representation of the input map which library can use. */ private fun convertToMap(values: Map): HashMap { val map = HashMap() for (key in values.keys) { val value = values[key] if (value != null) { map[key] = value.toString() } } return map } /** * Converts a map of Variant_String_Double_Boolean to a HashMap. * * @param values The map to be converted. * @return A HashMap representation of the input map which library can use. */ private fun convertToMapAny(values: Map): java.util.HashMap? { val map = HashMap() for (key in values.keys) { val value = values[key] if (value != null) { map[key] = value } } return map } /** * Gets the current activity from the ReactApplicationContext. * * @return The current activity or null if not available. */ private fun getCurrentActivity(): android.app.Activity? { return reactContext.currentActivity } /** * Add focus listener to handle EditText UI control. * * @param textView Input TextView. * @param accessibilityID Accessibility ID(virtual id). * @param activity Current activity. */ fun addFocusAndRegister(textView: TextView, accessibilityID: String?, activity: Activity) { textView.onFocusChangeListener = OnFocusChangeListener { v: View, hasFocus: Boolean -> if (hasFocus) { val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.showSoftInput(v, InputMethodManager.SHOW_FORCED) val keyboardView = KeyboardView(v.context.applicationContext, null) if (TextUtils.isEmpty(accessibilityID)) { logEvent(keyboardView, TLF_UI_KEYBOARD_DID_SHOW_NOTIFICATION) logEvent(v, TLF_ON_FOCUS_CHANGE_IN) } else { logEvent(keyboardView, TLF_UI_KEYBOARD_DID_SHOW_NOTIFICATION, accessibilityID!!) logEvent(v, TLF_ON_FOCUS_CHANGE_IN, accessibilityID!!) } } else { logEvent(v, TLF_ON_FOCUS_CHANGE_OUT) val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.hideSoftInputFromWindow(v.windowToken, 0) val keyboardView = KeyboardView(v.context.applicationContext, null) if (TextUtils.isEmpty(accessibilityID)) { logEvent(keyboardView, TLF_UI_KEYBOARD_DID_HIDE_NOTIFICATION) } else { logEvent(keyboardView, TLF_UI_KEYBOARD_DID_HIDE_NOTIFICATION, accessibilityID!!) } } } registerFormField(textView, activity) } // override fun onWindowFocusChanged(hasFocus: Boolean) { // if (!reactContext.hasActiveCatalystInstance()) { // logEvent("WindowFocus", "Context is not ready. Skipping onWindowFocusChanged.") // return // } // // // Handle window focus change // if (hasFocus) { // logEvent("WindowFocus", "Window gained focus") // } else { // logEvent("WindowFocus", "Window lost focus") // } // } // override fun onWindowFocusChanged(hasFocus: Boolean) { // super.onWindowFocusChanged(hasFocus) // // val reactHost = (application as MainApplication).reactHost // if (reactHost != null) { // reactHost.onWindowFocusChange(hasFocus) // } // } fun onWindowFocusChanged(hasFocus: Boolean) { if (!reactContext.hasActiveCatalystInstance()) { // logEvent("WindowFocus", "Context is not ready. Skipping onWindowFocusChanged.") return } // Handle window focus change if (hasFocus) { // logEvent("WindowFocus", "Window gained focus") } else { // logEvent("WindowFocus", "Window lost focus") } } /** * Used when host resumes. */ override fun onHostResume() { // Initialize Connect library, and hook into activity lifecycle events to help detect if app is in background if (!reactContext.hasActiveCatalystInstance()) { // logEvent("Lifecycle", "onHostResume skipped: ReactContext is not ready") return } val activity = getCurrentActivity() if (activity == null) { // logEvent("Lifecycle", "onHostResume skipped: Activity is null") return } if (!isEnabled()) { if (getApplication() == null) { init((reactContext.applicationContext as Application)) } enable() } onResume(activity, null) } /** * Used when host gets paused. */ override fun onHostPause() { val activity = getCurrentActivity() if (activity == null) { // logEvent("Lifecycle", "onHostPause skipped: Activity is null") return } Connect.onPause(activity, null) } /** * Used when host gets destroyed. */ override fun onHostDestroy() { val activity = getCurrentActivity() if (activity == null) { // logEvent("Lifecycle", "onHostDestroy skipped: Activity is null") return } Tealeaf.onDestroy(activity, null) // Uncomment if Connect.onDestroy is needed // Connect.onDestroy(activity, null) } companion object { const val TAG = "AcousticConnectRN" } }