1 package expo.modules.device 2 3 import android.app.ActivityManager 4 import android.app.UiModeManager 5 import android.content.Context 6 import android.content.res.Configuration 7 import android.os.Build 8 import android.os.SystemClock 9 import android.provider.Settings 10 import android.util.DisplayMetrics 11 import android.view.WindowManager 12 import com.facebook.device.yearclass.YearClass 13 import expo.modules.core.utilities.EmulatorUtilities 14 import expo.modules.kotlin.exception.Exceptions 15 import expo.modules.kotlin.modules.Module 16 import expo.modules.kotlin.modules.ModuleDefinition 17 import java.io.File 18 import kotlin.math.pow 19 import kotlin.math.sqrt 20 21 class DeviceModule : Module() { 22 // Keep this enum in sync with JavaScript 23 enum class DeviceType(val JSValue: Int) { 24 UNKNOWN(0), 25 PHONE(1), 26 TABLET(2), 27 DESKTOP(3), 28 TV(4); 29 } 30 31 private val context: Context 32 get() = appContext.reactContext ?: throw Exceptions.ReactContextLost() 33 34 override fun definition() = ModuleDefinition { 35 Name("ExpoDevice") 36 37 Constants { 38 return@Constants mapOf( 39 "isDevice" to !isRunningOnEmulator, 40 "brand" to Build.BRAND, 41 "manufacturer" to Build.MANUFACTURER, 42 "modelName" to Build.MODEL, 43 "designName" to Build.DEVICE, 44 "productName" to Build.DEVICE, 45 "deviceYearClass" to deviceYearClass, 46 "totalMemory" to run { 47 val memoryInfo = ActivityManager.MemoryInfo() 48 (context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).getMemoryInfo(memoryInfo) 49 memoryInfo.totalMem 50 }, 51 "deviceType" to run { 52 getDeviceType(context).JSValue 53 }, 54 "supportedCpuArchitectures" to Build.SUPPORTED_ABIS?.takeIf { it.isNotEmpty() }, 55 "osName" to systemName, 56 "osVersion" to Build.VERSION.RELEASE, 57 "osBuildId" to Build.DISPLAY, 58 "osInternalBuildId" to Build.ID, 59 "osBuildFingerprint" to Build.FINGERPRINT, 60 "platformApiLevel" to Build.VERSION.SDK_INT, 61 "deviceName" to if (Build.VERSION.SDK_INT <= 31) 62 Settings.Secure.getString(context.contentResolver, "bluetooth_name") 63 else 64 Settings.Global.getString(context.contentResolver, Settings.Global.DEVICE_NAME) 65 ) 66 } 67 68 AsyncFunction("getDeviceTypeAsync") { 69 return@AsyncFunction getDeviceType(context).JSValue 70 } 71 72 AsyncFunction("getUptimeAsync") { 73 return@AsyncFunction SystemClock.uptimeMillis().toDouble() 74 } 75 76 AsyncFunction("getMaxMemoryAsync") { 77 val maxMemory = Runtime.getRuntime().maxMemory() 78 return@AsyncFunction if (maxMemory != Long.MAX_VALUE) maxMemory.toDouble() else -1 79 } 80 81 AsyncFunction("isRootedExperimentalAsync") { 82 val isRooted: Boolean 83 val isDevice = !isRunningOnEmulator 84 85 val buildTags = Build.TAGS 86 isRooted = if (isDevice && buildTags != null && buildTags.contains("test-keys")) { 87 true 88 } else { 89 if (File("/system/app/Superuser.apk").exists()) { 90 true 91 } else { 92 isDevice && File("/system/xbin/su").exists() 93 } 94 } 95 96 return@AsyncFunction isRooted 97 } 98 99 AsyncFunction("isSideLoadingEnabledAsync") { 100 return@AsyncFunction if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { 101 Settings.Global.getInt( 102 context.applicationContext.contentResolver, 103 Settings.Global.INSTALL_NON_MARKET_APPS, 104 0 105 ) == 1 106 } else { 107 context.applicationContext.packageManager.canRequestPackageInstalls() 108 } 109 } 110 111 AsyncFunction("getPlatformFeaturesAsync") { 112 val allFeatures = context.applicationContext.packageManager.systemAvailableFeatures 113 return@AsyncFunction allFeatures.filterNotNull().map { it.name } 114 } 115 116 AsyncFunction("hasPlatformFeatureAsync") { feature: String -> 117 return@AsyncFunction context.applicationContext.packageManager.hasSystemFeature(feature) 118 } 119 } 120 121 private val deviceYearClass: Int 122 get() = YearClass.get(context) 123 124 private val systemName: String 125 get() { 126 return Build.VERSION.BASE_OS.takeIf { it.isNotEmpty() } ?: "Android" 127 } 128 129 companion object { 130 private val isRunningOnEmulator: Boolean 131 get() = EmulatorUtilities.isRunningOnEmulator() 132 133 private fun getDeviceType(context: Context): DeviceType { 134 // Detect TVs via UI mode (Android TVs) or system features (Fire TV). 135 if (context.applicationContext.packageManager.hasSystemFeature("amazon.hardware.fire_tv")) { 136 return DeviceType.TV 137 } 138 139 val uiManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager? 140 if (uiManager != null && uiManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) { 141 return DeviceType.TV 142 } 143 144 val deviceTypeFromResourceConfiguration = getDeviceTypeFromResourceConfiguration(context) 145 return if (deviceTypeFromResourceConfiguration != DeviceType.UNKNOWN) { 146 deviceTypeFromResourceConfiguration 147 } else { 148 getDeviceTypeFromPhysicalSize(context) 149 } 150 } 151 152 // Device type based on the smallest screen width quantifier 153 // https://developer.android.com/guide/topics/resources/providing-resources#SmallestScreenWidthQualifier 154 private fun getDeviceTypeFromResourceConfiguration(context: Context): DeviceType { 155 val smallestScreenWidthDp = context.resources.configuration.smallestScreenWidthDp 156 157 return if (smallestScreenWidthDp == Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED) { 158 DeviceType.UNKNOWN 159 } else if (smallestScreenWidthDp >= 600) { 160 DeviceType.TABLET 161 } else { 162 DeviceType.PHONE 163 } 164 } 165 166 private fun getDeviceTypeFromPhysicalSize(context: Context): DeviceType { 167 // Find the current window manager, if none is found we can't measure the device physical size. 168 val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager? 169 ?: return DeviceType.UNKNOWN 170 171 // Get display metrics to see if we can differentiate phones and tablets. 172 val widthInches: Double 173 val heightInches: Double 174 175 // windowManager.defaultDisplay was marked as deprecated in API level 30 (Android R) and above 176 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 177 val windowBounds = windowManager.currentWindowMetrics.bounds 178 val densityDpi = context.resources.configuration.densityDpi 179 widthInches = windowBounds.width() / densityDpi.toDouble() 180 heightInches = windowBounds.height() / densityDpi.toDouble() 181 } else { 182 val metrics = DisplayMetrics() 183 @Suppress("DEPRECATION") 184 windowManager.defaultDisplay.getRealMetrics(metrics) 185 widthInches = metrics.widthPixels / metrics.xdpi.toDouble() 186 heightInches = metrics.heightPixels / metrics.ydpi.toDouble() 187 } 188 189 // Calculate physical size. 190 val diagonalSizeInches = sqrt(widthInches.pow(2.0) + heightInches.pow(2.0)) 191 192 return if (diagonalSizeInches in 3.0..6.9) { 193 // Devices in a sane range for phones are considered to be phones. 194 DeviceType.PHONE 195 } else if (diagonalSizeInches > 6.9 && diagonalSizeInches <= 18.0) { 196 // Devices larger than a phone and in a sane range for tablets are tablets. 197 DeviceType.TABLET 198 } else { 199 // Otherwise, we don't know what device type we're on. 200 DeviceType.UNKNOWN 201 } 202 } 203 } 204 } 205