package com.bigcrunch.ads.core import android.content.Context import android.content.res.Configuration import android.os.Build import android.util.DisplayMetrics import android.view.WindowManager import com.bigcrunch.ads.internal.BCLogger import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import java.util.Locale import java.util.TimeZone /** * DeviceContext collects and provides device/app context for analytics * * Gathers information about: * - Device type (phone, tablet) * - Operating system and version * - App name and version * - Screen dimensions * - Language and locale * - Timezone * * All values are cached at initialization for consistent reporting. */ internal class DeviceContext private constructor(context: Context) { companion object { private const val TAG = "DeviceContext" internal const val SDK_VERSION = "0.21.0" @Volatile private var instance: DeviceContext? = null /** * Initialize the DeviceContext singleton * * Must be called once during SDK initialization. * * @param context Application context */ fun initialize(context: Context) { if (instance == null) { synchronized(this) { if (instance == null) { instance = DeviceContext(context.applicationContext) } } } } /** * Get the DeviceContext instance * * @return The singleton instance * @throws IllegalStateException if not initialized */ fun getInstance(): DeviceContext { return instance ?: throw IllegalStateException( "DeviceContext not initialized. Call DeviceContext.initialize() first." ) } /** * Reset the singleton instance (for testing only) */ internal fun resetForTesting() { synchronized(this) { instance = null } } } // MARK: - Device Properties /** Device type: "phone" or "tablet" */ val deviceType: String /** Device manufacturer (e.g., "Samsung") */ val deviceManufacturer: String = Build.MANUFACTURER /** Device model (e.g., "SM-G991B") */ val deviceModel: String = Build.MODEL /** Operating system name (always "Android") */ val osName: String = "Android" /** Operating system version (e.g., "13") */ val osVersion: String = Build.VERSION.RELEASE /** Android API level (e.g., 33) */ val apiLevel: Int = Build.VERSION.SDK_INT /** Screen width in pixels */ val screenWidth: Int /** Screen height in pixels */ val screenHeight: Int /** Screen density (e.g., 2.75) */ val screenDensity: Float // MARK: - App Properties /** App package name (e.g., "com.example.app") */ val appPackageName: String /** App display name */ val appName: String /** App version name (e.g., "1.2.3") */ val appVersion: String /** App version code (e.g., 42) */ val appVersionCode: Long // MARK: - Locale Properties /** User's preferred language code (e.g., "en") */ val languageCode: String /** User's country/region code (e.g., "US") */ val countryCode: String /** User's region/state (e.g., "CA", "NY") - may be empty if not available */ val region: String /** User's timezone identifier (e.g., "America/New_York") */ val timezone: String // MARK: - SDK Properties /** SDK version */ val sdkVersion: String = SDK_VERSION /** SDK platform (always "android") */ val sdkPlatform: String = "android" init { val appContext = context.applicationContext // Device type val screenLayout = appContext.resources.configuration.screenLayout val screenSize = screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK deviceType = if (screenSize >= Configuration.SCREENLAYOUT_SIZE_LARGE) "tablet" else "phone" // Screen dimensions (with fallbacks for test environments) val screenMetrics = try { val windowManager = appContext.getSystemService(Context.WINDOW_SERVICE) as? WindowManager if (windowManager != null) { val displayMetrics = DisplayMetrics() @Suppress("DEPRECATION") windowManager.defaultDisplay.getMetrics(displayMetrics) Triple(displayMetrics.widthPixels, displayMetrics.heightPixels, displayMetrics.density) } else { // Fallback for test/mock environments val resources = appContext.resources Triple(resources.displayMetrics.widthPixels, resources.displayMetrics.heightPixels, resources.displayMetrics.density) } } catch (e: Exception) { // Fallback for test/mock environments BCLogger.w(TAG, "Failed to get screen dimensions, using defaults") Triple(0, 0, 1.0f) } screenWidth = screenMetrics.first screenHeight = screenMetrics.second screenDensity = screenMetrics.third // App info appPackageName = appContext.packageName val packageInfo = try { appContext.packageManager.getPackageInfo(appPackageName, 0) } catch (e: Exception) { BCLogger.e(TAG, "Failed to get package info", e) null } appVersion = packageInfo?.versionName ?: "0.0.0" appVersionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { packageInfo?.longVersionCode ?: 0L } else { @Suppress("DEPRECATION") (packageInfo?.versionCode ?: 0).toLong() } // App name from label val applicationInfo = try { appContext.packageManager.getApplicationInfo(appPackageName, 0) } catch (e: Exception) { null } appName = applicationInfo?.let { appContext.packageManager.getApplicationLabel(it).toString() } ?: "unknown" // Locale info val locale = Locale.getDefault() languageCode = locale.language countryCode = locale.country.ifEmpty { "US" } timezone = TimeZone.getDefault().id // Region/state extraction - Android doesn't provide direct access, leave empty for now // Could be populated from IP geolocation or user settings in future region = "" BCLogger.d(TAG, "Initialized - $deviceType $deviceManufacturer $deviceModel Android $osVersion") } // MARK: - Convenience Methods /** * Get web schema compatible fields (flat structure) * * @return Map of flattened web schema fields */ fun getWebSchemaFields(): Map { return mapOf( "browser" to "BigCrunch Android SDK $sdkVersion", "device" to deviceModel, "os" to fullOSString, "country" to countryCode, "region" to timezone ) } /** * Get browser field for web schema (e.g., "BigCrunch Android SDK 1.0.0") */ fun getBrowserField(): String { return "BigCrunch Android SDK $sdkVersion" } /** * Get mobile-specific device context fields (for optional inclusion) * * @return Map of mobile-specific context values */ fun getMobileSpecificFields(): Map { return mapOf( "device_manufacturer" to deviceManufacturer, "device_model" to deviceModel, "os_version" to osVersion, "api_level" to apiLevel, "screen_width" to screenWidth, "screen_height" to screenHeight, "screen_density" to screenDensity, "app_package_name" to appPackageName, "app_name" to appName, "app_version" to appVersion, "app_version_code" to appVersionCode, "language_code" to languageCode, "timezone" to timezone, "sdk_version" to sdkVersion, "sdk_platform" to sdkPlatform ) } /** * Get a map representation for analytics events (legacy format) * DEPRECATED: Use getWebSchemaFields() for new analytics events * * @return Map of device context values */ fun toMap(): Map { return mapOf( "device_type" to deviceType, "device_manufacturer" to deviceManufacturer, "device_model" to deviceModel, "os_name" to osName, "os_version" to osVersion, "api_level" to apiLevel, "screen_width" to screenWidth, "screen_height" to screenHeight, "screen_density" to screenDensity, "app_package_name" to appPackageName, "app_name" to appName, "app_version" to appVersion, "app_version_code" to appVersionCode, "language_code" to languageCode, "country_code" to countryCode, "region" to region, "timezone" to timezone, "sdk_version" to sdkVersion, "sdk_platform" to sdkPlatform ) } /** * Get the full OS string (e.g., "Android 13") */ val fullOSString: String get() = "$osName $osVersion" /** * Get screen dimensions string (e.g., "1080x1920") */ val screenDimensionsString: String get() = "${screenWidth}x$screenHeight" } /** * Data class for including device context in analytics events */ @JsonClass(generateAdapter = true) internal data class DeviceContextData( @Json(name = "device_type") val deviceType: String, @Json(name = "device_manufacturer") val deviceManufacturer: String, @Json(name = "device_model") val deviceModel: String, @Json(name = "os_name") val osName: String, @Json(name = "os_version") val osVersion: String, @Json(name = "api_level") val apiLevel: Int, @Json(name = "screen_width") val screenWidth: Int, @Json(name = "screen_height") val screenHeight: Int, @Json(name = "app_package_name") val appPackageName: String, @Json(name = "app_name") val appName: String, @Json(name = "app_version") val appVersion: String, @Json(name = "language_code") val languageCode: String, @Json(name = "country_code") val countryCode: String, @Json(name = "region") val region: String, @Json(name = "timezone") val timezone: String, @Json(name = "sdk_version") val sdkVersion: String, @Json(name = "sdk_platform") val sdkPlatform: String ) { companion object { /** * Create from the DeviceContext singleton */ fun fromInstance(): DeviceContextData { val ctx = DeviceContext.getInstance() return DeviceContextData( deviceType = ctx.deviceType, deviceManufacturer = ctx.deviceManufacturer, deviceModel = ctx.deviceModel, osName = ctx.osName, osVersion = ctx.osVersion, apiLevel = ctx.apiLevel, screenWidth = ctx.screenWidth, screenHeight = ctx.screenHeight, appPackageName = ctx.appPackageName, appName = ctx.appName, appVersion = ctx.appVersion, languageCode = ctx.languageCode, countryCode = ctx.countryCode, region = ctx.region, timezone = ctx.timezone, sdkVersion = ctx.sdkVersion, sdkPlatform = ctx.sdkPlatform ) } } }