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 if (Build.VERSION.SDK_INT < 23) { 127 "Android" 128 } else { 129 Build.VERSION.BASE_OS.takeIf { it.isNotEmpty() } ?: "Android" 130 } 131 } 132 133 companion object { 134 private val isRunningOnEmulator: Boolean 135 get() = EmulatorUtilities.isRunningOnEmulator() 136 137 private fun getDeviceType(context: Context): DeviceType { 138 // Detect TVs via UI mode (Android TVs) or system features (Fire TV). 139 if (context.applicationContext.packageManager.hasSystemFeature("amazon.hardware.fire_tv")) { 140 return DeviceType.TV 141 } 142 143 val uiManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager? 144 if (uiManager != null && uiManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) { 145 return DeviceType.TV 146 } 147 148 val deviceTypeFromResourceConfiguration = getDeviceTypeFromResourceConfiguration(context) 149 return if (deviceTypeFromResourceConfiguration != DeviceType.UNKNOWN) { 150 deviceTypeFromResourceConfiguration 151 } else { 152 getDeviceTypeFromPhysicalSize(context) 153 } 154 } 155 156 // Device type based on the smallest screen width quantifier 157 // https://developer.android.com/guide/topics/resources/providing-resources#SmallestScreenWidthQualifier 158 private fun getDeviceTypeFromResourceConfiguration(context: Context): DeviceType { 159 val smallestScreenWidthDp = context.resources.configuration.smallestScreenWidthDp 160 161 return if (smallestScreenWidthDp == Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED) { 162 DeviceType.UNKNOWN 163 } else if (smallestScreenWidthDp >= 600) { 164 DeviceType.TABLET 165 } else { 166 DeviceType.PHONE 167 } 168 } 169 170 private fun getDeviceTypeFromPhysicalSize(context: Context): DeviceType { 171 // Find the current window manager, if none is found we can't measure the device physical size. 172 val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager? 173 ?: return DeviceType.UNKNOWN 174 175 // Get display metrics to see if we can differentiate phones and tablets. 176 val widthInches: Double 177 val heightInches: Double 178 179 // windowManager.defaultDisplay was marked as deprecated in API level 30 (Android R) and above 180 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 181 val windowBounds = windowManager.currentWindowMetrics.bounds 182 val densityDpi = context.resources.configuration.densityDpi 183 widthInches = windowBounds.width() / densityDpi.toDouble() 184 heightInches = windowBounds.height() / densityDpi.toDouble() 185 } else { 186 val metrics = DisplayMetrics() 187 @Suppress("DEPRECATION") 188 windowManager.defaultDisplay.getRealMetrics(metrics) 189 widthInches = metrics.widthPixels / metrics.xdpi.toDouble() 190 heightInches = metrics.heightPixels / metrics.ydpi.toDouble() 191 } 192 193 // Calculate physical size. 194 val diagonalSizeInches = sqrt(widthInches.pow(2.0) + heightInches.pow(2.0)) 195 196 return if (diagonalSizeInches in 3.0..6.9) { 197 // Devices in a sane range for phones are considered to be phones. 198 DeviceType.PHONE 199 } else if (diagonalSizeInches > 6.9 && diagonalSizeInches <= 18.0) { 200 // Devices larger than a phone and in a sane range for tablets are tablets. 201 DeviceType.TABLET 202 } else { 203 // Otherwise, we don't know what device type we're on. 204 DeviceType.UNKNOWN 205 } 206 } 207 } 208 } 209