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 "supportedCpuArchitectures" to Build.SUPPORTED_ABIS?.takeIf { it.isNotEmpty() }, 52 "osName" to systemName, 53 "osVersion" to Build.VERSION.RELEASE, 54 "osBuildId" to Build.DISPLAY, 55 "osInternalBuildId" to Build.ID, 56 "osBuildFingerprint" to Build.FINGERPRINT, 57 "platformApiLevel" to Build.VERSION.SDK_INT, 58 "deviceName" to if (Build.VERSION.SDK_INT <= 31) 59 Settings.Secure.getString(context.contentResolver, "bluetooth_name") 60 else 61 Settings.Global.getString(context.contentResolver, Settings.Global.DEVICE_NAME) 62 ) 63 } 64 65 AsyncFunction("getDeviceTypeAsync") { 66 return@AsyncFunction getDeviceType(context).JSValue 67 } 68 69 AsyncFunction("getUptimeAsync") { 70 return@AsyncFunction SystemClock.uptimeMillis().toDouble() 71 } 72 73 AsyncFunction("getMaxMemoryAsync") { 74 val maxMemory = Runtime.getRuntime().maxMemory() 75 return@AsyncFunction if (maxMemory != Long.MAX_VALUE) maxMemory.toDouble() else -1 76 } 77 78 AsyncFunction("isRootedExperimentalAsync") { 79 val isRooted: Boolean 80 val isDevice = !isRunningOnEmulator 81 82 val buildTags = Build.TAGS 83 isRooted = if (isDevice && buildTags != null && buildTags.contains("test-keys")) { 84 true 85 } else { 86 if (File("/system/app/Superuser.apk").exists()) { 87 true 88 } else { 89 isDevice && File("/system/xbin/su").exists() 90 } 91 } 92 93 return@AsyncFunction isRooted 94 } 95 96 AsyncFunction("isSideLoadingEnabledAsync") { 97 return@AsyncFunction if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { 98 Settings.Global.getInt( 99 context.applicationContext.contentResolver, 100 Settings.Global.INSTALL_NON_MARKET_APPS, 101 0 102 ) == 1 103 } else { 104 context.applicationContext.packageManager.canRequestPackageInstalls() 105 } 106 } 107 108 AsyncFunction("getPlatformFeaturesAsync") { 109 val allFeatures = context.applicationContext.packageManager.systemAvailableFeatures 110 return@AsyncFunction allFeatures.filterNotNull().map { it.name } 111 } 112 113 AsyncFunction("hasPlatformFeatureAsync") { feature: String -> 114 return@AsyncFunction context.applicationContext.packageManager.hasSystemFeature(feature) 115 } 116 } 117 118 private val deviceYearClass: Int 119 get() = YearClass.get(context) 120 121 private val systemName: String 122 get() { 123 return if (Build.VERSION.SDK_INT < 23) { 124 "Android" 125 } else { 126 Build.VERSION.BASE_OS.takeIf { it.isNotEmpty() } ?: "Android" 127 } 128 } 129 130 companion object { 131 private val isRunningOnEmulator: Boolean 132 get() = EmulatorUtilities.isRunningOnEmulator() 133 134 private fun getDeviceType(context: Context): DeviceType { 135 // Detect TVs via UI mode (Android TVs) or system features (Fire TV). 136 if (context.applicationContext.packageManager.hasSystemFeature("amazon.hardware.fire_tv")) { 137 return DeviceType.TV 138 } 139 140 val uiManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager? 141 if (uiManager != null && uiManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) { 142 return DeviceType.TV 143 } 144 145 val deviceTypeFromResourceConfiguration = getDeviceTypeFromResourceConfiguration(context) 146 return if (deviceTypeFromResourceConfiguration != DeviceType.UNKNOWN) { 147 deviceTypeFromResourceConfiguration 148 } else { 149 getDeviceTypeFromPhysicalSize(context) 150 } 151 } 152 153 // Device type based on the smallest screen width quantifier 154 // https://developer.android.com/guide/topics/resources/providing-resources#SmallestScreenWidthQualifier 155 private fun getDeviceTypeFromResourceConfiguration(context: Context): DeviceType { 156 val smallestScreenWidthDp = context.resources.configuration.smallestScreenWidthDp 157 158 return if (smallestScreenWidthDp == Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED) { 159 DeviceType.UNKNOWN 160 } else if (smallestScreenWidthDp >= 600) { 161 DeviceType.TABLET 162 } else { 163 DeviceType.PHONE 164 } 165 } 166 167 private fun getDeviceTypeFromPhysicalSize(context: Context): DeviceType { 168 // Find the current window manager, if none is found we can't measure the device physical size. 169 val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager? 170 ?: return DeviceType.UNKNOWN 171 172 // Get display metrics to see if we can differentiate phones and tablets. 173 val widthInches: Double 174 val heightInches: Double 175 176 // windowManager.defaultDisplay was marked as deprecated in API level 30 (Android R) and above 177 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 178 val windowBounds = windowManager.currentWindowMetrics.bounds 179 val densityDpi = context.resources.configuration.densityDpi 180 widthInches = windowBounds.width() / densityDpi.toDouble() 181 heightInches = windowBounds.height() / densityDpi.toDouble() 182 } else { 183 val metrics = DisplayMetrics() 184 @Suppress("DEPRECATION") 185 windowManager.defaultDisplay.getRealMetrics(metrics) 186 widthInches = metrics.widthPixels / metrics.xdpi.toDouble() 187 heightInches = metrics.heightPixels / metrics.ydpi.toDouble() 188 } 189 190 // Calculate physical size. 191 val diagonalSizeInches = sqrt(widthInches.pow(2.0) + heightInches.pow(2.0)) 192 193 return if (diagonalSizeInches in 3.0..6.9) { 194 // Devices in a sane range for phones are considered to be phones. 195 DeviceType.PHONE 196 } else if (diagonalSizeInches > 6.9 && diagonalSizeInches <= 18.0) { 197 // Devices larger than a phone and in a sane range for tablets are tablets. 198 DeviceType.TABLET 199 } else { 200 // Otherwise, we don't know what device type we're on. 201 DeviceType.UNKNOWN 202 } 203 } 204 } 205 } 206