package com.segmentanalyticsreactnative import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.content.Intent import android.content.pm.PackageInfo import android.content.res.Resources import android.media.MediaDrm import android.net.ConnectivityManager import android.net.NetworkCapabilities import android.net.Uri import android.os.Build import android.util.Log import androidx.core.content.pm.PackageInfoCompat import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule import com.sovranreactnative.SovranModule import java.lang.Exception import java.util.* import java.security.MessageDigest import java.util.UUID enum class ConnectionType { Cellular, Unknown, Wifi } @ReactModule(name="AnalyticsReactNative") class AnalyticsReactNativeModule : ReactContextBaseJavaModule, ActivityEventListener, LifecycleEventListener { var onInitialized: () -> Unit = {}; constructor(reactContext: ReactApplicationContext) : super(reactContext) { this.pInfo = reactContext.packageManager.getPackageInfo(reactContext.packageName, 0) // Listen for new intents when app is in the background reactContext.addActivityEventListener(this) // Listen for resume events when it is cold started, or in the background reactContext.addLifecycleEventListener(this) } override fun initialize() { super.initialize() onInitialized() } private var isColdLaunch = true private val pInfo: PackageInfo override fun getName(): String { return "AnalyticsReactNative" } private fun getBuildNumber(): String { return PackageInfoCompat.getLongVersionCode(pInfo).toString() } fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) } /** * Workaround for not able to get device id on Android 10 or above using DRM API * {@see https://stackoverflow.com/questions/58103580/android-10-imei-no-longer-available-on-api-29-looking-for-alternatives} * {@see https://developer.android.com/training/articles/user-data-ids} */ private fun getUniqueId(collectDeviceId : Boolean): String? { if (collectDeviceId) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) return null val WIDEVINE_UUID = UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L) var wvDrm: MediaDrm? = null try { wvDrm = MediaDrm(WIDEVINE_UUID) val wideVineId = wvDrm.getPropertyByteArray(MediaDrm.PROPERTY_DEVICE_UNIQUE_ID) val md = MessageDigest.getInstance("SHA-256") md.update(wideVineId) return md.digest().toHexString() } catch (e: Exception) { return null } finally { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { wvDrm?.close() } else { wvDrm?.release() } } } return null } private fun getConnectionType(context: Context): ConnectionType { val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? var result: ConnectionType = ConnectionType.Unknown if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { cm?.run { cm.getNetworkCapabilities(cm.activeNetwork)?.run { if (hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { result = ConnectionType.Wifi } else if (hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { result = ConnectionType.Cellular } else { result = ConnectionType.Unknown } } } } else { cm?.run { cm.activeNetworkInfo?.run { if (type == ConnectivityManager.TYPE_WIFI) { result = ConnectionType.Wifi } else if (type == ConnectivityManager.TYPE_MOBILE) { result = ConnectionType.Cellular } else { result = ConnectionType.Unknown } } } } return result } @ReactMethod fun getContextInfo(config: ReadableMap, promise: Promise) { val appName: String = reactApplicationContext.applicationInfo.loadLabel(reactApplicationContext.packageManager).toString() val appVersion: String = pInfo.versionName.toString() val buildNumber = getBuildNumber() val bundleId = reactApplicationContext.packageName val connectionType: ConnectionType = getConnectionType(reactApplicationContext) val timezone: TimeZone = TimeZone.getDefault() val currentLocale: Locale = Locale.getDefault() val locale: String = "${currentLocale.language}-${currentLocale.country}" val screenWidth = Resources.getSystem().displayMetrics.widthPixels val screenHeight = Resources.getSystem().displayMetrics.heightPixels val screenDensity = Resources.getSystem().displayMetrics.density; val contextInfo: WritableMap = Arguments.createMap() if (config.hasKey("collectDeviceId") && config.getBoolean("collectDeviceId")) { val fallbackDeviceId = UUID.randomUUID().toString() val deviceId = getUniqueId(config.hasKey("collectDeviceId") && config.getBoolean("collectDeviceId"))?: fallbackDeviceId contextInfo.putString("deviceId", deviceId) } contextInfo.putString("appName", appName) contextInfo.putString("appVersion", appVersion) contextInfo.putString("buildNumber", buildNumber) contextInfo.putString("bundleId", bundleId) contextInfo.putString("deviceName", Build.DEVICE) contextInfo.putString("deviceType", "android") contextInfo.putString("manufacturer", Build.MANUFACTURER) contextInfo.putString("model", Build.MODEL) contextInfo.putString("timezone", timezone.id) contextInfo.putString("locale", locale) contextInfo.putString("networkType", connectionType.toString().lowercase(currentLocale)) contextInfo.putString("osName", "Android") contextInfo.putString("osVersion", Build.VERSION.RELEASE) contextInfo.putInt("screenWidth", screenWidth) contextInfo.putInt("screenHeight", screenHeight) contextInfo.putDouble("screenDensity", screenDensity.toDouble()) promise.resolve(contextInfo) } fun getReferrer(activity: Activity): Uri? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { activity.referrer } else { null } } fun trackDeepLinks(intent: Intent?) { if (intent == null || intent.data == null) { Log.d(name, "No Intent data found") return } val properties = Hashtable() if (reactApplicationContext.currentActivity == null) { Log.d(name, "No activity found") return } val referrer = getReferrer(reactApplicationContext.currentActivity!!) if (referrer != null) { val referringApplication = referrer.toString() properties["referring_application"] = referringApplication } val uri = intent.data try { properties["url"] = uri.toString() for (parameter in uri!!.queryParameterNames) { val value = uri.getQueryParameter(parameter) if (value != null && value.trim { it <= ' ' }.isNotEmpty()) { properties[parameter] = value } } } catch (e: Exception) { // handle error Log.d(name, "Error getting URL: ${e.message}") return } Log.d(name, "Sending Deeplink data to store: uri=${uri}, referrer=${referrer}") val sovran = reactApplicationContext.getNativeModule(SovranModule::class.java) sovran?.dispatch("add-deepLink-data", properties) } fun setAnonymousId(anonymousId: String) { val properties = Hashtable() properties["anonymousId"] = anonymousId val currentContext = getReactApplicationContext() if (currentContext != null) { val sovran = currentContext.getNativeModule(SovranModule::class.java) sovran?.dispatch("add-anonymous-id", properties) } } override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) { // Do nothing } override fun onNewIntent(intent: Intent) { Log.d(name, "onNewIntent = ${intent}") trackDeepLinks(intent) } override fun onHostResume() { if (reactApplicationContext.currentActivity != null && isColdLaunch) { isColdLaunch = false Log.d(name, "onHostResume = ${reactApplicationContext.currentActivity!!.intent}") trackDeepLinks(reactApplicationContext.currentActivity!!.intent) } } override fun onHostPause() { // Do nothing } override fun onHostDestroy() { isColdLaunch = true // Do nothing } }